arboard-3.6.1/.cargo_vcs_info.json0000644000000001360000000000100124550ustar { "git": { "sha1": "a3750c79a5d63f3987317e03c412b7d9dffdc2af" }, "path_in_vcs": "" }arboard-3.6.1/.github/workflows/test.yml000064400000000000000000000066301046102023000163510ustar 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.6.1/.gitignore000064400000000000000000000000251046102023000132320ustar 00000000000000target .vscode *.png arboard-3.6.1/Cargo.lock0000644000000544440000000000100104430ustar # 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.6.1" 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.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 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.6.1/Cargo.toml0000644000000102250000000000100104530ustar # 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.6.1" build = false autolib = 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" [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 = ["windows-sys/Win32_Graphics_Gdi"] wl-clipboard-rs = ["dep:wl-clipboard-rs"] [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" [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", "bmp", ] optional = true default-features = false [target."cfg(windows)".dependencies.log] version = "0.4" [target."cfg(windows)".dependencies.windows-sys] version = ">=0.52.0, <0.61.0" features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", "Win32_UI_Shell", ] arboard-3.6.1/Cargo.toml.orig000064400000000000000000000046771046102023000141520ustar 00000000000000[package] name = "arboard" version = "3.6.1" description = "Image and text handling for the OS clipboard." repository = "https://github.com/1Password/arboard" license = "MIT OR Apache-2.0" 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 = ["windows-sys/Win32_Graphics_Gdi"] 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.61.0", features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_System_Ole", "Win32_UI_Shell", ] } clipboard-win = { version = "5.3.1", features = ["std"] } log = "0.4" image = { version = "0.25", optional = true, default-features = false, features = [ "png", "bmp" ] } [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.6.1/LICENSE-APACHE.txt000064400000000000000000000236761046102023000140250ustar 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.6.1/LICENSE-MIT.txt000064400000000000000000000020711046102023000135170ustar 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.6.1/README.md000064400000000000000000000143111046102023000125240ustar 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. Please note that this is not an official 1Password product. Feature requests will be considered like any other volunteer-based crate. ## GNU/Linux ### Backend Support By default, `arboard`'s backend on Linux supports X11 (or XWayland implementations) and uses that for managing the various Linux clipboard variants. This supports the majority of desktop environments that exist in the wild today. `arboard` will use the `Clipboard` selection by default, but the [LinuxClipboardKind](https://docs.rs/arboard/latest/arboard/enum.LinuxClipboardKind.html) selector lets you operate on the `Primary` or `Secondary` clipboard selections (if supported). However, Wayland is becoming the majority default as of 2025. Some distributions are even considering the removal of X by default. To support Wayland correctly, `arboard` users should enable the `wayland-data-control` feature. If enabled, it will be prioritized over the X clipboard. Wayland support is not enabled by default because it may be counterintuitive to some users: it relies on the data-control protocol extension(s), which _are not_ supported by all Wayland compositors. You can check compositor support on `wayland.app`: - [ext-data-control-v1](https://wayland.app/protocols/ext-data-control-v1) - [wlr-data-control-unstable-v1](https://wayland.app/protocols/wlr-data-control-unstable-v1) If you or a user's desktop doesn't support these protocols, `arboard` won't function in a pure Wayland environment. It is recommended to enable `XWayland` for these cases. If your app runs inside an isolated sandbox, such as Flatpak or Snap, you'll need to expose the X11 socket to the application _in addition_ to the Wayland communication interface. ### Clipboard Ownership Some apps and users may notice that sometimes values copied to a Linux clipboard with this crate vanish before anyone gets the chance to paste, or just aren't available when you expect them to be. The root behind these problems is _selection ownership_. X11 and Wayland put the responsibility for answering paste requests and serving data on the application which originally copied it onto the clipboard. This usually means the app using `arboard`. Nothing you copy to the clipboard is sent anywhere to start. It stays inside `arboard` until something else on the system requests it, which is very different to how the clipboard works on other platforms like macOS or Windows. Note that `arboard` may attempt to warn you about these conditions when compiled in debug mode, to improve the debugging experience. Even if you don't see these warnings, you should double check the lifetime of the `Clipboard` in your code. In some cases, an environment may have a clipboard manager installed. These services monitor the clipboard contents and do their best to retain a copy when needed to smooth over clipboard ownership changes. A clipboar manager can make contents available even after a process previously owning it exits. In order to keep the contents around longer, make sure that you don't `Drop` your `Clipboard` object right away or terminate the copying process too fast. This is why, at times, adding a call to `sleep()` near the set operation makes it behave more reliabily: the background thread `arboard` uses for serving clipboard contents has more time to run and let other apps (including clipboard managers) make requests for the contents. However `sleep` isn't the recommended approach. If your application is exiting, you must make sure there is a clipboard manager running on the system. If nothing is listening for the clipboard ownership transfer, or made a copy previously, the data will be lost. Note that this isn't a complete guarantee as races are possible if your program's main thread is exiting. If you would like to fully synchronize the clipboard "paste" before exiting, you can use the [wait](https://docs.rs/arboard/latest/arboard/trait.SetExtLinux.html#tymethod.wait) method when setting contents on the clipboard. This will block the calling thread until another app has requested, and then received, the data. If your application is longer-running (ie a GUI, TUI, etc), it is highly recommended that you either store the `Clipboard` object in some long-lived data structure (like app context, etc) or utilize `wait` method mentioned above, and/or threading to make sure another app can request the clipboard data later. We welcome suggestions to improve on the above issues in ways that don't degrade other use cases. ## Example ```rust 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); } ``` ## Credits This crate is a combined effort by 1Password staff and `@ArturKovacs`, the crate's past maintainer. #### License Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. #### History: Yet another clipboard crate This crate started out as a fork of `rust-clipboard`. The reason for forking is due to the former crate not being maintained any longer. At this point, `arboard`'s backends and public APIs have diverged a lot. `arboard`'s original maintainer noted that "I don't know why this is happening but while it is, we might as well just start naming the clipboard crates after ourselves. This one is arboard which stands for Artur's clipboard.". arboard-3.6.1/examples/daemonize.rs000064400000000000000000000020471046102023000154070ustar 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.6.1/examples/get_image.rs000064400000000000000000000002461046102023000153540ustar 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.6.1/examples/hello_world.rs000064400000000000000000000004741046102023000157500ustar 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.6.1/examples/set_get_html.rs000064400000000000000000000010531046102023000161060ustar 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.6.1/examples/set_image.rs000064400000000000000000000005121046102023000153640ustar 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.6.1/rustfmt.toml000064400000000000000000000001371046102023000136470ustar 00000000000000hard_tabs=true use_field_init_shorthand=true use_small_heuristics="Max" use_try_shorthand=true arboard-3.6.1/src/common.rs000064400000000000000000000137261046102023000137030ustar 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 another party. /// /// This "other party" could be a different process or it could be within /// the same program. So for example you may get this error when trying /// 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 another party."), Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), } } } 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.6.1/src/lib.rs000064400000000000000000000372531046102023000131620ustar 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::{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) } /// Completes the "set" operation by placing a list of file paths onto the clipboard. pub fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { self.platform.file_list(file_list) } } /// A builder for an operation that clears the data from the clipboard. #[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); } } { let mut ctx = Clipboard::new().unwrap(); let this_dir = env!("CARGO_MANIFEST_DIR"); let paths = &[ PathBuf::from(this_dir).join("README.md"), PathBuf::from(this_dir).join("Cargo.toml"), ]; ctx.set().file_list(paths).unwrap(); assert_eq!(ctx.get().file_list().unwrap().as_slice(), paths); } #[cfg(feature = "image-data")] { let mut ctx = Clipboard::new().unwrap(); #[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.6.1/src/platform/linux/mod.rs000064400000000000000000000343241046102023000161520ustar 00000000000000use std::{ borrow::Cow, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, time::Instant, }; #[cfg(feature = "wayland-data-control")] use log::{trace, warn}; use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS}; #[cfg(feature = "image-data")] use crate::ImageData; use crate::{common::private, Error}; // Magic strings used in `Set::exclude_from_history()` on linux const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint"; const KDE_EXCLUSION_HINT: &[u8] = b"secret"; mod x11; #[cfg(feature = "wayland-data-control")] 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: Vec) -> Vec { uri_list .split(|char| *char == b'\n') .filter_map(|line| line.strip_prefix(b"file://")) .filter_map(|s| percent_decode(s).decode_utf8().ok()) .map(|decoded| PathBuf::from(decoded.as_ref())) .collect() } fn paths_to_uri_list(file_list: &[impl AsRef]) -> Result { // The characters that require encoding, which includes £ and € but they can't be added to the set. const ASCII_SET: &AsciiSet = &CONTROLS .add(b'#') .add(b';') .add(b'?') .add(b'[') .add(b']') .add(b' ') .add(b'\"') .add(b'%') .add(b'<') .add(b'>') .add(b'\\') .add(b'^') .add(b'`') .add(b'{') .add(b'|') .add(b'}'); file_list .iter() .filter_map(|path| { path.as_ref().canonicalize().ok().map(|path| { format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET)) }) }) .reduce(|uri_list, uri| uri_list + "\n" + &uri) .ok_or(Error::ConversionFailure) } /// Clipboard selection /// /// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This /// 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, exclude_from_history: bool, } impl<'clipboard> Set<'clipboard> { pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self { Self { clipboard, wait: WaitConfig::default(), selection: LinuxClipboardKind::Clipboard, exclude_from_history: false, } } pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { match self.clipboard { Clipboard::X11(clipboard) => { clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history) } #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => { clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history) } } } pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> { match self.clipboard { Clipboard::X11(clipboard) => { clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history) } #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => { clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history) } } } #[cfg(feature = "image-data")] pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> { match self.clipboard { Clipboard::X11(clipboard) => { clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history) } #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => { clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history) } } } pub(crate) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { match self.clipboard { Clipboard::X11(clipboard) => clipboard.set_file_list( file_list, self.selection, self.wait, self.exclude_from_history, ), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.set_file_list( file_list, self.selection, self.wait, self.exclude_from_history, ), } } } /// Linux specific extensions to the [`Set`](super::Set) builder. 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; /// Excludes the data which will be set on the clipboard from being added to /// the desktop clipboard managers' histories by adding the MIME-Type `x-kde-passwordMangagerHint` /// to the clipboard's selection data. /// /// This is the most widely adopted convention on Linux. fn exclude_from_history(self) -> Self; } impl SetExtLinux for crate::Set<'_> { 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 } fn exclude_from_history(mut self) -> Self { self.platform.exclude_from_history = true; 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> { match self.clipboard { Clipboard::X11(clipboard) => clipboard.clear(selection), #[cfg(feature = "wayland-data-control")] Clipboard::WlDataControl(clipboard) => clipboard.clear(selection), } } } /// 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 = [ "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").into()), paths); } } arboard-3.6.1/src/platform/linux/wayland.rs000064400000000000000000000167561046102023000170430ustar 00000000000000use std::{ borrow::Cow, io::Read, path::{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, paths_to_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME, }; use crate::common::Error; #[cfg(feature = "image-data")] use crate::common::ImageData; #[cfg(feature = "image-data")] const MIME_PNG: &str = "image/png"; const MIME_URI: &str = "text/uri-list"; pub(crate) struct Clipboard {} impl TryInto for LinuxClipboardKind { 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), } } } fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec) { if exclude_from_history { sources.push(MimeSource { source: Source::Bytes(Box::from(KDE_EXCLUSION_HINT)), mime_type: MimeType::Specific(String::from(KDE_EXCLUSION_MIME)), }); } } fn handle_copy_error(e: copy::Error) -> Error { match e { CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), } } fn handle_paste_error(e: paste::Error) -> Error { match e { PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported, other => into_unknown(other), } } fn handle_clipboard_read) -> Result>( selection: LinuxClipboardKind, mime: paste::MimeType, into_requested_data: F, ) -> Result { let result = get_contents(selection.try_into()?, Seat::Unspecified, mime); match result { Ok((mut pipe, _)) => { let mut buffer = vec![]; pipe.read_to_end(&mut buffer).map_err(into_unknown)?; into_requested_data(buffer) } Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => { Err(Error::ContentNotAvailable) } Err(err) => Err(handle_paste_error(err)), } } impl Clipboard { pub(crate) fn new() -> Result { // Check if it's possible to communicate with the wayland compositor match is_primary_selection_supported() { // We don't care if the primary clipboard is supported or not, `wl-clipboard-rs` will fail // if not and we don't want to duplicate more of their logic. Ok(_) => Ok(Self {}), Err(e) => Err(into_unknown(e)), } } pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> { let selection = selection.try_into()?; copy::clear(selection, copy::Seat::All).map_err(handle_copy_error) } pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { handle_clipboard_read(selection, paste::MimeType::Text, |contents| { String::from_utf8(contents).map_err(|_| Error::ConversionFailure) }) } pub(crate) fn set_text( &self, text: Cow<'_, str>, selection: LinuxClipboardKind, wait: WaitConfig, exclude_from_history: bool, ) -> Result<(), Error> { let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); sources.push(MimeSource { source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()), mime_type: MimeType::Text, }); add_clipboard_exclusions(exclude_from_history, &mut sources); opts.copy_multi(sources).map_err(handle_copy_error) } pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| { String::from_utf8(contents).map_err(|_| Error::ConversionFailure) }) } pub(crate) fn set_html( &self, html: Cow<'_, str>, alt: Option>, selection: LinuxClipboardKind, wait: WaitConfig, exclude_from_history: bool, ) -> Result<(), Error> { let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let mut sources = { let cap = [true, alt.is_some(), exclude_from_history] .map(|v| usize::from(v as u8)) .iter() .sum(); Vec::with_capacity(cap) }; if let Some(alt) = alt { sources.push(MimeSource { source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()), mime_type: MimeType::Text, }); } sources.push(MimeSource { source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()), mime_type: MimeType::Specific(String::from("text/html")), }); add_clipboard_exclusions(exclude_from_history, &mut sources); opts.copy_multi(sources).map_err(handle_copy_error) } #[cfg(feature = "image-data")] pub(crate) fn get_image( &mut self, selection: LinuxClipboardKind, ) -> Result, Error> { use std::io::Cursor; handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| { let image = image::io::Reader::new(Cursor::new(buffer)) .with_guessed_format() .map_err(|_| Error::ConversionFailure)? .decode() .map_err(|_| Error::ConversionFailure)?; let image = image.into_rgba8(); Ok(ImageData { width: image.width() as usize, height: image.height() as usize, bytes: image.into_raw().into(), }) }) } #[cfg(feature = "image-data")] pub(crate) fn set_image( &mut self, image: ImageData, selection: LinuxClipboardKind, wait: WaitConfig, exclude_from_history: bool, ) -> Result<(), Error> { let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let image = encode_as_png(&image)?; let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); sources.push(MimeSource { source: Source::Bytes(image.into()), mime_type: MimeType::Specific(String::from(MIME_PNG)), }); add_clipboard_exclusions(exclude_from_history, &mut sources); opts.copy_multi(sources).map_err(handle_copy_error) } pub(crate) fn get_file_list( &mut self, selection: LinuxClipboardKind, ) -> Result, Error> { handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| { Ok(paths_from_uri_list(contents)) }) } pub(crate) fn set_file_list( &self, file_list: &[impl AsRef], selection: LinuxClipboardKind, wait: WaitConfig, exclude_from_history: bool, ) -> Result<(), Error> { let files = paths_to_uri_list(file_list)?; let mut opts = Options::new(); opts.foreground(matches!(wait, WaitConfig::Forever)); opts.clipboard(selection.try_into()?); let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); sources.push(MimeSource { source: Source::Bytes(files.into_bytes().into_boxed_slice()), mime_type: MimeType::Specific(String::from(MIME_URI)), }); add_clipboard_exclusions(exclude_from_history, &mut sources); opts.copy_multi(sources).map_err(handle_copy_error) } } arboard-3.6.1/src/platform/linux/x11.rs000064400000000000000000001115531046102023000160040ustar 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::{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, paths_to_uri_list, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME, }; #[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", X_KDE_PASSWORDMANAGERHINT: KDE_EXCLUSION_MIME.as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. 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 when this selection was last changed by us /// for both use with the below condvar and logging. mutex: Mutex>, /// A condvar that is notified when the contents of this clipboard are changed. /// /// This is associated with `Self::mutex`. 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), }) } /// Performs a "clear" operation on the clipboard, which is implemented by /// relinquishing the selection to revert its owner to `None`. This gracefully /// and comformly informs the X server and any clipboard managers that the /// data was no longer valid and won't be offered from our window anymore. /// /// See `ask_clipboard_manager_to_request_our_data` for more details on why /// this is important and specification references. fn clear(&self, selection: LinuxClipboardKind) -> Result<()> { let selection = self.atom_of(selection); self.server .conn .set_selection_owner(NONE, selection, Time::CURRENT_TIME) .map_err(into_unknown)?; self.server.conn.flush().map_err(into_unknown) } fn write( &self, data: Vec, clipboard_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; // Just setting the data, and the `serve_requests` will take care of the rest. let selection = self.selection_of(clipboard_selection); let mut data_guard = selection.data.write(); *data_guard = Some(data); // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data // changes. self.server .conn .set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME) .map_err(|_| Error::ClipboardOccupied)?; self.server.conn.flush().map_err(into_unknown)?; // 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(); // Record the time we modify the selection. *guard = Some(Instant::now()); // Notify any existing waiting threads that we have changed the data in the selection. // It is important that the mutex is locked to prevent this notification getting lost. 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 data = self.selection_of(selection).data.read(); let (data_targets, excluded) = if let Some(data_list) = &*data { // Estimation based on current data types, plus the other UTF-8 ones, plus `SAVE_TARGETS`. let mut targets = Vec::with_capacity(data_list.len() + 3); let mut excluded = false; for data in data_list { targets.push(data.format); if data.format == self.atoms.UTF8_STRING { // 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); } if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT { excluded = true; } } (targets, excluded) } else { // If there's no data, we advertise an empty list of targets. (Vec::with_capacity(2), false) }; let mut targets = data_targets; targets.push(self.atoms.TARGETS); // NB: `SAVE_TARGETS` in this context is a marker atom which infomrs the clipboard manager // we support this operation and _may_ use it in the future. To try and keep the manager's // expectations/assumptions (if any) about when we will invoke this handoff, we go ahead and // skip advertising support for the save operation entirely when the data was marked as // sensitive. // // Note that even if we don't advertise it, some managers may respond to it anyways so this is // only half of exclusion handling. See `ask_clipboard_manager_to_request_our_data` for more. if !excluded { targets.push(self.atoms.SAVE_TARGETS); } self.server .conn .change_property32( 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(()); } // Per the `ClipboardManager` specification, only the `CLIPBOARD` target is // to be saved from other X clients, so if the caller set the `Primary` (or `Secondary`) clipboard, // we wouldn't expect any clipboard manager to save that anyway. let selection = LinuxClipboardKind::Clipboard; if !self.is_owner(selection)? { // We are not owning the clipboard, nothing to do. return Ok(()); } match &*self.selection_of(selection).data.read() { Some(data) => { // If the data we are serving intended to be excluded, then don't bother asking the clipboard // manager to save it. This is for several reasons: // 1. Its counter-intuitive because the caller asked for this data to be minimally retained. // 2. Regardless of if `SAVE_TARGETS` was advertised, we have to assume the manager may be saving history // in a more proactive way and that would also be entirely dependent on it seeing the exclusion MIME before this. // 3. Due to varying behavior in clipboard managers (some save prior to `SAVE_TARGETS`), it may just // generate unnessecary warning logs in our handoff path even when we know a well-behaving manager isn't // trying to save our sensitive data and that is misleading to users. if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) { // This step is the most important. Without it, some clipboard managers may think that our process // crashed since the X window is destroyed without changing the selection owner first and try to save data. // // While this shouldn't need to happen based only on ICCCM 2.3.1 ("Voluntarily Giving Up Selection Ownership"), // its documentation that destorying the owner window or terminating also reverts the owner to `None` doesn't // reflect how desktop environment's X servers work in reality. // // By removing the owner, the manager doesn't think it needs to pick up our window's data serving once // its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint. if let Err(e) = self.clear(selection) { warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!"); // This is still not an error because we werent going to handoff anything to the manager. } return Ok(()); } } 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(()); } unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.") } } 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. if let Err(e) = context.handle_selection_request(event) { error!("Failed to handle selection request: {e}"); continue; } // if we are in the progress of saving to the clipboard manager // make sure we save that we have finished writing 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 }) } fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec) { if exclude_from_history { data.push(ClipboardData { bytes: KDE_EXCLUSION_HINT.to_vec(), format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT, }) } } pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> { self.inner.clear(selection) } pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result { let formats = [ self.inner.atoms.UTF8_STRING, 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, exclude_from_history: bool, ) -> Result<()> { let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); data.push(ClipboardData { bytes: message.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, }); self.add_clipboard_exclusions(exclude_from_history, &mut data); self.inner.write(data, selection, wait) } 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, exclude_from_history: bool, ) -> Result<()> { let mut data = { let cap = [true, alt.is_some(), exclude_from_history] .map(|v| usize::from(v as u8)) .iter() .sum(); Vec::with_capacity(cap) }; if let Some(alt_text) = alt { data.push(ClipboardData { bytes: alt_text.into_owned().into_bytes(), format: self.inner.atoms.UTF8_STRING, }); } data.push(ClipboardData { bytes: html.into_owned().into_bytes(), format: self.inner.atoms.HTML, }); self.add_clipboard_exclusions(exclude_from_history, &mut data); self.inner.write(data, selection, wait) } #[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, exclude_from_history: bool, ) -> Result<()> { let encoded = encode_as_png(&image)?; let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }); self.add_clipboard_exclusions(exclude_from_history, &mut data); self.inner.write(data, selection, wait) } pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result> { let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?; Ok(paths_from_uri_list(result.bytes)) } pub(crate) fn set_file_list( &self, file_list: &[impl AsRef], selection: LinuxClipboardKind, wait: WaitConfig, exclude_from_history: bool, ) -> Result<()> { let files = paths_to_uri_list(file_list)?; let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 }); data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST }); self.add_clipboard_exclusions(exclude_from_history, &mut data); self.inner.write(data, selection, wait) } } impl Drop for Clipboard { 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 { let GlobalClipboard { inner, server_handle } = global_cb; drop(inner); if let Err(e) = server_handle.join() { // Let's try extracting the error message let message; if let Some(msg) = e.downcast_ref::<&'static str>() { 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."); } } // By this point we've dropped the Global's reference to `Inner` and the background // thread has exited which means it also dropped its reference. Therefore `self.inner` should // be the last strong count. // // Note: The following is all best effort and is only for logging. Nothing is guaranteed to execute // or log. #[cfg(debug_assertions)] if let Some(inner) = Arc::get_mut(&mut self.inner) { use std::io::IsTerminal; let mut change_timestamps = Vec::with_capacity(2); let mut collect_changed = |sel: &mut Mutex>| { if let Some(changed) = sel.get_mut() { change_timestamps.push(*changed); } }; collect_changed(&mut inner.clipboard.mutex); collect_changed(&mut inner.primary.mutex); collect_changed(&mut inner.secondary.mutex); change_timestamps.sort(); if let Some(last) = change_timestamps.last() { let elapsed = last.elapsed().as_millis(); // This number has no meaning, its just a guess for how long // might be reasonable to give a clipboard manager a chance to // save contents based ~roughly on the handoff timeout. if elapsed > 100 { return; } // If the app isn't running in a terminal don't print, use log instead. // Printing has a higher chance of being seen though, so its our default. // Its also close enough to a `debug_assert!` that it shouldn't come across strange. let msg = format!("Clipboard was dropped very quickly after writing ({elapsed}ms); clipboard managers may not have seen the contents\nConsider keeping `Clipboard` in more persistent state somewhere or keeping the contents alive longer using `SetLinuxExt` and/or threads."); if std::io::stderr().is_terminal() { eprintln!("{msg}"); } else { log::warn!("{msg}"); } } } } } } } arboard-3.6.1/src/platform/mod.rs000064400000000000000000000005711046102023000150100ustar 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.6.1/src/platform/osx.rs000064400000000000000000000325251046102023000150460ustar 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::{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) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { self.clipboard.clear(); let uri_list = file_list .iter() .filter_map(|path| { path.as_ref().canonicalize().ok().and_then(|abs_path| { abs_path.to_str().map(|str| { let url = unsafe { NSURL::fileURLWithPath(&NSString::from_str(str)) }; ProtocolObject::from_retained(url) }) }) }) .collect::>(); if uri_list.is_empty() { return Err(Error::ConversionFailure); } let objects = NSArray::from_retained_slice(&uri_list); let success = unsafe { self.clipboard.pasteboard.writeObjects(&objects) }; add_clipboard_exclusions(self.clipboard, self.exclude_from_history); if success { Ok(()) } else { Err(Error::unknown("NSPasteboard#writeObjects: returned false")) } } } pub(crate) struct Clear<'clipboard> { 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.6.1/src/platform/windows.rs000064400000000000000000001063641046102023000157320ustar 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, io, marker::PhantomData, os::windows::{fs::OpenOptionsExt, io::AsRawHandle}, path::{Path, PathBuf}, thread, time::Duration, }; use windows_sys::Win32::{ Foundation::{GetLastError, GlobalFree, HANDLE, HGLOBAL, POINT, S_OK}, Storage::FileSystem::{GetFinalPathNameByHandleW, FILE_FLAG_BACKUP_SEMANTICS, VOLUME_NAME_DOS}, System::{ DataExchange::SetClipboardData, Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND}, Ole::CF_HDROP, }, UI::Shell::{PathCchStripPrefix, DROPFILES}, }; #[cfg(feature = "image-data")] mod image_data { use super::*; use crate::common::ScopeGuard; use image::codecs::bmp::BmpDecoder; use image::codecs::png::PngDecoder; use image::codecs::png::PngEncoder; use image::DynamicImage; use image::ExtendedColorType; use image::ImageDecoder; use image::ImageEncoder; use std::{convert::TryInto, mem::size_of, ptr::copy_nonoverlapping}; use windows_sys::Win32::{ Graphics::Gdi::{ DeleteObject, BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, HGDIOBJ, LCS_GM_IMAGES, }, System::Ole::CF_DIBV5, }; 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(()) } } // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header // According to the docs, when bV5Compression is BI_RGB, "the high byte in each DWORD // is not used". // This seems to not be respected in the real world. For example, Chrome, and Chromium // & Electron-based programs send us BI_RGB headers, but with bitCount=32 - and important // transparency bytes in the alpha channel. // // Apparently, it's our job as the consumer to do the right thing. This method fiddles // with the header a bit in these cases, then `image` handles the rest. fn maybe_tweak_header(dibv5: &mut [u8]) { assert!(dibv5.len() >= size_of::()); let src = dibv5.as_mut_ptr().cast::(); let mut header = unsafe { std::ptr::read_unaligned(src) }; if header.bV5BitCount == 32 && header.bV5Compression == BI_RGB && header.bV5AlphaMask == 0xff000000 { header.bV5Compression = BI_BITFIELDS; if header.bV5RedMask == 0 && header.bV5GreenMask == 0 && header.bV5BlueMask == 0 { header.bV5RedMask = 0xff0000; header.bV5GreenMask = 0xff00; header.bV5BlueMask = 0xff; } unsafe { std::ptr::write_unaligned(src, header) }; } } pub(super) fn read_cf_dibv5(dibv5: &mut [u8]) -> Result, Error> { // The DIBV5 format is a BITMAPV5HEADER followed by the pixel data according to // https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats 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.")); } maybe_tweak_header(dibv5); let decoder = BmpDecoder::new_without_file_header(std::io::Cursor::new(&*dibv5)) .map_err(|_| Error::ConversionFailure)?; let (width, height) = decoder.dimensions(); let bytes = DynamicImage::from_decoder(decoder) .map_err(|_| Error::ConversionFailure)? .into_rgba8() .into_raw(); Ok(ImageData { width: width as usize, height: height as usize, bytes: bytes.into() }) } pub(super) fn read_png(data: &[u8]) -> Result, Error> { let decoder = PngDecoder::new(std::io::Cursor::new(data)).map_err(|_| Error::ConversionFailure)?; let (width, height) = decoder.dimensions(); let bytes = DynamicImage::from_decoder(decoder) .map_err(|_| Error::ConversionFailure)? .into_rgba8() .into_raw(); Ok(ImageData { width: width as usize, height: height as usize, bytes: bytes.into() }) } /// Converts the RGBA (u8) pixel data into the bitmap-native ARGB (u32) /// format in-place. /// /// 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] #[cfg(test)] unsafe fn win_to_rgba(bytes: &mut [u8]) -> Vec { // Check safety invariants to catch obvious bugs. debug_assert_eq!(bytes.len() % 4, 0); 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); } #[test] fn firefox_dibv5() { // A 5x5 sample of https://commons.wikimedia.org/wiki/File:PNG_transparency_demonstration_1.png let mut raw = vec![ 124, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 66, 71, 82, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 47, 144, 42, 68, 110, 48, 74, 66, 52, 74, 49, 57, 80, 55, 0, 36, 53, 138, 45, 79, 98, 52, 82, 58, 56, 84, 52, 62, 91, 58, 0, 37, 64, 129, 48, 88, 88, 54, 90, 54, 60, 96, 55, 66, 104, 62, 0, 40, 75, 120, 50, 96, 74, 55, 99, 51, 62, 106, 57, 68, 113, 62, 0, 42, 89, 107, 50, 104, 60, 57, 108, 49, 64, 114, 56, 71, 123, 65, 0, ]; let before = raw.clone(); let image = read_cf_dibv5(&mut raw).unwrap(); // Not expecting any header fiddling to happen here. This is a bitmap in 24-bit format, with a header // that says as much assert_eq!(raw, before); assert_eq!(image.width, 5); assert_eq!(image.height, 5); const EXPECTED: &[u8] = &[ 107, 89, 42, 255, 60, 104, 50, 255, 49, 108, 57, 255, 56, 114, 64, 255, 65, 123, 71, 255, 120, 75, 40, 255, 74, 96, 50, 255, 51, 99, 55, 255, 57, 106, 62, 255, 62, 113, 68, 255, 129, 64, 37, 255, 88, 88, 48, 255, 54, 90, 54, 255, 55, 96, 60, 255, 62, 104, 66, 255, 138, 53, 36, 255, 98, 79, 45, 255, 58, 82, 52, 255, 52, 84, 56, 255, 58, 91, 62, 255, 144, 47, 36, 255, 110, 68, 42, 255, 66, 74, 48, 255, 49, 74, 52, 255, 55, 80, 57, 255, ]; assert_eq!(image.bytes, EXPECTED); } #[test] fn chrome_dibv5() { // A 5x5 sample of https://commons.wikimedia.org/wiki/File:PNG_transparency_demonstration_1.png // (interestingly, the same sample as in the Firefox test - despite the pixel data being // materially different!) let mut raw = vec![ 124, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 32, 110, 105, 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 38, 145, 192, 38, 65, 111, 158, 46, 73, 68, 107, 50, 73, 50, 92, 55, 79, 55, 100, 31, 46, 139, 190, 41, 76, 100, 152, 49, 81, 60, 110, 53, 83, 53, 108, 60, 91, 60, 118, 32, 59, 131, 187, 44, 86, 89, 150, 51, 89, 56, 121, 57, 95, 57, 127, 63, 103, 63, 139, 35, 71, 122, 186, 46, 95, 76, 150, 52, 99, 54, 136, 59, 105, 59, 146, 65, 113, 65, 156, 37, 86, 109, 184, 46, 103, 63, 155, 52, 107, 53, 152, 60, 114, 60, 162, 68, 123, 68, 174, ]; let before = raw.clone(); let image = read_cf_dibv5(&mut raw).unwrap(); // Chrome's header is dodgy. Expect that we fiddled with it. assert_ne!(raw, before); assert_eq!(image.width, 5); assert_eq!(image.height, 5); const EXPECTED: &[u8] = &[ 109, 86, 37, 184, 63, 103, 46, 155, 53, 107, 52, 152, 60, 114, 60, 162, 68, 123, 68, 174, 122, 71, 35, 186, 76, 95, 46, 150, 54, 99, 52, 136, 59, 105, 59, 146, 65, 113, 65, 156, 131, 59, 32, 187, 89, 86, 44, 150, 56, 89, 51, 121, 57, 95, 57, 127, 63, 103, 63, 139, 139, 46, 31, 190, 100, 76, 41, 152, 60, 81, 49, 110, 53, 83, 53, 108, 60, 91, 60, 118, 145, 38, 32, 192, 111, 65, 38, 158, 68, 73, 46, 107, 50, 73, 50, 92, 55, 79, 55, 100, ]; assert_eq!(image.bytes, EXPECTED); } } unsafe fn global_alloc(bytes: usize) -> Result { let hdata = GlobalAlloc(GHND, bytes); if hdata.is_null() { Err(last_error("Could not allocate global memory object")) } else { Ok(hdata) } } unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> { let data_ptr = GlobalLock(hmem).cast::(); if data_ptr.is_null() { Err(last_error("Could not lock the global memory object")) } else { Ok(data_ptr) } } unsafe fn global_unlock_checked(hdata: HGLOBAL) { // If the memory object is unlocked after decrementing the lock count, the function // returns zero and GetLastError returns NO_ERROR. If it fails, the return value is // zero and GetLastError returns a value other than NO_ERROR. if GlobalUnlock(hdata) == 0 { let err = io::Error::last_os_error(); if err.raw_os_error() != Some(0) { log::error!("Failed calling GlobalUnlock when writing data: {}", err); } } } fn last_error(message: &str) -> Error { let os_error = io::Error::last_os_error(); Error::unknown(format!("{message}: {os_error}")) } /// An abstraction trait over the different ways a Win32 function may return /// a value with a failure marker. /// /// This trait helps unify error handling across varying `windows-sys` versions, /// providing a consistent interface for representing NULL values. trait ResultValue: Sized { const NULL: Self; fn failure(self) -> bool; } // windows-sys >= 0.59 impl ResultValue for *mut T { const NULL: Self = core::ptr::null_mut(); fn failure(self) -> bool { self == Self::NULL } } // `windows-sys` 0.52 impl ResultValue for isize { const NULL: Self = 0; fn failure(self) -> bool { self == Self::NULL } } /// A shim clipboard type that can have operations performed with it, but /// does not represent an open clipboard itself. /// /// 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, Error> { // 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); } // NB: Its important that whatever functionality decodes the text buffer from the clipboard // uses `WideCharToMultiByte` with `CP_UTF8` (or an equivalent) in order to handle when both "text" // and a locale identifier were placed on the clipboard. It is probable this occurs when an application // is running with a codepage that isn't the current system's, such as under a locale emulator. // // In these cases, Windows decodes the text buffer with whatever codepage that identifier is for // when creating the `CF_UNICODETEXT` buffer. Therefore, the buffer could then be in any format, // not nessecarily wide UTF-16. We need to then undo that, taking the wide data and mapping it into // the UTF-8 space as best as possible. // // (locale-specific text data, locale id) -> app -> system -> arboard (locale-specific text data) -> UTF-8 let mut out = Vec::new(); clipboard_win::raw::get_string(&mut out).map_err(|_| Error::ContentNotAvailable)?; String::from_utf8(out).map_err(|_| Error::ConversionFailure) } pub(crate) fn html(self) -> Result { 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> { let _clipboard_assertion = self.clipboard?; let mut data = Vec::new(); let png_format: Option = clipboard_win::register_format("PNG").map(From::from); if let Some(id) = png_format.filter(|&id| clipboard_win::is_format_avail(id)) { // Looks like PNG is available! Let's try it clipboard_win::raw::get_vec(id, &mut data) .map_err(|_| Error::unknown("failed to read clipboard PNG data"))?; return image_data::read_png(&data); } if !clipboard_win::is_format_avail(clipboard_win::formats::CF_DIBV5) { return Err(Error::ContentNotAvailable); } clipboard_win::raw::get_vec(clipboard_win::formats::CF_DIBV5, &mut data) .map_err(|_| Error::unknown("failed to read clipboard image data"))?; image_data::read_cf_dibv5(&mut data) } 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(()) } pub(crate) fn file_list(self, file_list: &[impl AsRef]) -> Result<(), Error> { const DROPFILES_HEADER_SIZE: usize = std::mem::size_of::(); let clipboard_assertion = self.clipboard?; // https://learn.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop // CF_HDROP consists of an STGMEDIUM structure that contains a global memory object. // The structure's hGlobal member points to the resulting data: // | DROPFILES | FILENAME | NULL | ... | nth FILENAME | NULL | NULL | let dropfiles = DROPFILES { pFiles: DROPFILES_HEADER_SIZE as u32, pt: POINT { x: 0, y: 0 }, fNC: 0, fWide: 1, }; let mut data_len = DROPFILES_HEADER_SIZE; let paths: Vec<_> = file_list .iter() .filter_map(|path| { to_final_path_wide(path.as_ref()).map(|wide| { // Windows uses wchar_t which is 16 bit data_len += wide.len() * std::mem::size_of::(); wide }) }) .collect(); if paths.is_empty() { return Err(Error::ConversionFailure); } // Add space for the final null character data_len += std::mem::size_of::(); unsafe { let h_global = global_alloc(data_len)?; let data_ptr = global_lock(h_global)?; (data_ptr as *mut DROPFILES).write(dropfiles); let mut ptr = data_ptr.add(DROPFILES_HEADER_SIZE) as *mut u16; for wide_path in paths { std::ptr::copy_nonoverlapping::(wide_path.as_ptr(), ptr, wide_path.len()); ptr = ptr.add(wide_path.len()); } // Write final null character ptr.write(0); global_unlock_checked(h_global); if SetClipboardData(CF_HDROP.into(), h_global as HANDLE).failure() { GlobalFree(h_global); return Err(last_error("SetClipboardData failed with error")); } } add_clipboard_exclusions( clipboard_assertion, self.exclude_from_monitoring, self.exclude_from_cloud, self.exclude_from_history, ) } } fn add_clipboard_exclusions( _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!( "{h_version}{h_start_html}{n_start_html:010}{h_end_html}{n_end_html:010}{h_start_frag}{n_start_frag:010}{h_end_frag}{n_end_frag:010}{c_start_frag}{ctn}{c_end_frag}" ) } /// Given a file path attempt to open it and call GetFinalPathNameByHandleW, /// on success return the final path as a NULL terminated u16 Vec fn to_final_path_wide(p: &Path) -> Option> { let file = std::fs::OpenOptions::new() // No read or write permissions are necessary .access_mode(0) // This flag is so we can open directories too .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) .open(p) .ok()?; fill_utf16_buf( |buf, sz| unsafe { GetFinalPathNameByHandleW(file.as_raw_handle() as HANDLE, buf, sz, VOLUME_NAME_DOS) }, |buf| { let mut wide = Vec::with_capacity(buf.len() + 1); wide.extend_from_slice(buf); wide.push(0); let hr = unsafe { PathCchStripPrefix(wide.as_mut_ptr(), wide.len()) }; // On success truncate invalid data if hr == S_OK { if let Some(end) = wide.iter().position(|c| *c == 0) { // Retain NULL character wide.truncate(end + 1) } } wide }, ) } /// fn fill_utf16_buf(mut f1: F1, f2: F2) -> Option where F1: FnMut(*mut u16, u32) -> u32, F2: FnOnce(&[u16]) -> T, { // Start off with a stack buf but then spill over to the heap if we end up // needing more space. // // This initial size also works around `GetFullPathNameW` returning // incorrect size hints for some short paths: // https://github.com/dylni/normpath/issues/5 let mut stack_buf: [std::mem::MaybeUninit; 512] = [std::mem::MaybeUninit::uninit(); 512]; let mut heap_buf: Vec> = Vec::new(); unsafe { let mut n = stack_buf.len(); loop { let buf = if n <= stack_buf.len() { &mut stack_buf[..] } else { let extra = n - heap_buf.len(); heap_buf.reserve(extra); // We used `reserve` and not `reserve_exact`, so in theory we // may have gotten more than requested. If so, we'd like to use // it... so long as we won't cause overflow. n = heap_buf.capacity().min(u32::MAX as usize); // Safety: MaybeUninit does not need initialization heap_buf.set_len(n); &mut heap_buf[..] }; // This function is typically called on windows API functions which // will return the correct length of the string, but these functions // also return the `0` on error. In some cases, however, the // returned "correct length" may actually be 0! // // To handle this case we call `SetLastError` to reset it to 0 and // then check it again if we get the "0 error value". If the "last // error" is still 0 then we interpret it as a 0 length buffer and // not an actual error. windows_sys::Win32::Foundation::SetLastError(0); let k = match f1(buf.as_mut_ptr().cast::(), n as u32) { 0 if GetLastError() == 0 => 0, 0 => return None, n => n, } as usize; if k == n && GetLastError() == windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER { n = n.saturating_mul(2).min(u32::MAX as usize); } else if k > n { n = k; } else if k == n { // It is impossible to reach this point. // On success, k is the returned string length excluding the null. // On failure, k is the required buffer length including the null. // Therefore k never equals n. unreachable!(); } else { // Safety: First `k` values are initialized. let slice = std::slice::from_raw_parts(buf.as_ptr() as *const u16, k); return Some(f2(slice)); } } } } arboard-3.6.1/tools/debugger.entitlements000064400000000000000000000003431046102023000166260ustar 00000000000000 com.apple.security.get-task-allow arboard-3.6.1/tools/run_with_leaks.sh000075500000000000000000000012301046102023000157560ustar 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"