├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── init └── clipmenud.service.in ├── man ├── clipctl.1 ├── clipdel.1 ├── clipmenu.1 ├── clipmenu.conf.5 ├── clipmenud.1 └── clipserve.1 ├── src ├── clipctl.c ├── clipdel.c ├── clipmenu.c ├── clipmenud.c ├── clipserve.c ├── config.c ├── config.h ├── store.c ├── store.h ├── util.c ├── util.h ├── x.c └── x.h └── tests ├── test_store.c └── x_integration_tests /.clang-format: -------------------------------------------------------------------------------- 1 | BinPackArguments: true 2 | BreakStringLiterals: false 3 | ColumnLimit: 80 4 | IndentCaseLabels: true 5 | IndentPPDirectives: BeforeHash 6 | IndentWidth: 4 7 | InsertBraces: true 8 | PenaltyBreakComment: 0 9 | SortIncludes: true 10 | SpaceBeforeParens: ControlStatementsExceptForEachMacros 11 | UseTab: Never 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build_and_test: 3 | name: CI 4 | runs-on: ubuntu-latest 5 | 6 | steps: 7 | - uses: actions/checkout@v3 8 | 9 | - run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 10 | - run: sudo apt-get update 11 | 12 | - uses: awalsh128/cache-apt-pkgs-action@v1 13 | with: 14 | packages: gcc-10 clang clang-format clang-tidy cppcheck xvfb xsel libxfixes-dev libx11-dev 15 | version: 1.0 16 | 17 | - run: gcc --version 18 | 19 | - run: make clean analyse 20 | - run: make tests 21 | - run: make integration_tests 22 | env: 23 | NO_PID_NAMESPACE: 1 24 | 25 | on: 26 | push: 27 | pull_request: 28 | workflow_dispatch: 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/clipmenud 2 | src/clipctl 3 | src/clipdel 4 | src/clipmenu 5 | src/clipserve 6 | tests/test 7 | tests/test_store 8 | *.o 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-present Christopher Down 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -std=gnu11 -O2 -Wall -Wextra -Wshadow -Wpointer-arith \ 2 | -Wcast-align -Wmissing-prototypes -Wstrict-overflow -Wformat=2 \ 3 | -Wwrite-strings -Warray-bounds -Wstrict-prototypes \ 4 | -Wno-maybe-uninitialized \ 5 | -Werror $(CFLAGS) 6 | CPPFLAGS += -I/usr/X11R6/include -L/usr/X11R6/lib 7 | LDLIBS += -lX11 -lXfixes 8 | PREFIX ?= /usr/local 9 | bindir := $(PREFIX)/bin 10 | datarootdir := $(PREFIX)/share 11 | mandir := $(datarootdir)/man 12 | systemd_user_dir = $(DESTDIR)$(PREFIX)/lib/systemd/user 13 | debug_cflags := -D_FORTIFY_SOURCE=2 -fsanitize=leak -fsanitize=address \ 14 | -fsanitize=undefined -Og -ggdb -fno-omit-frame-pointer \ 15 | -fstack-protector-strong 16 | c_files := $(wildcard src/*.c) 17 | h_files := $(wildcard src/*.h) 18 | libs := $(filter $(c_files:.c=.o), $(h_files:.h=.o)) 19 | 20 | man1_files = clipctl.1 clipdel.1 clipmenu.1 clipmenud.1 clipserve.1 21 | man5_files = clipmenu.conf.5 22 | 23 | bins := clipctl clipmenud clipdel clipserve clipmenu 24 | 25 | all: $(addprefix src/,$(bins)) 26 | 27 | src/%: src/%.c $(libs) 28 | $(CC) $(CFLAGS) $(CPPFLAGS) $^ $(LDFLAGS) $(LDLIBS) -o $@ 29 | 30 | src/%.o: src/%.c src/%.h 31 | $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ 32 | 33 | debug: all 34 | debug: CFLAGS+=$(debug_cflags) 35 | 36 | install: all 37 | @for f in $(man1_files); do \ 38 | install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man1/$$f; \ 39 | done 40 | @for f in $(man5_files); do \ 41 | install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man5/$$f; \ 42 | done 43 | 44 | mkdir -p $(DESTDIR)$(bindir)/ 45 | install -pt $(DESTDIR)$(bindir)/ $(addprefix src/,$(bins)) 46 | mkdir -p $(systemd_user_dir) 47 | sed 's|@bindir@|$(bindir)|g' init/clipmenud.service.in > $(systemd_user_dir)/clipmenud.service 48 | 49 | uninstall: 50 | rm -f $(addprefix $(DESTDIR)$(PREFIX)/bin/,$(bins)) 51 | rm -f "$(DESTDIR)${PREFIX}/lib/systemd/user/clipmenud.service" 52 | 53 | clean: 54 | rm -f src/*.o src/*~ $(addprefix src/,$(bins)) 55 | 56 | clang_supports_unsafe_buffer_usage := $(shell clang -x c -c /dev/null -o /dev/null -Werror -Wunsafe-buffer-usage > /dev/null 2>&1; echo $$?) 57 | ifeq ($(clang_supports_unsafe_buffer_usage),0) 58 | extra_clang_flags := -Wno-unsafe-buffer-usage 59 | else 60 | extra_clang_flags := 61 | endif 62 | 63 | c_analyse_targets := $(c_files:%=%-analyse) 64 | h_analyse_targets := $(h_files:%=%-analyse) 65 | 66 | analyse: CFLAGS+=$(debug_cflags) 67 | analyse: $(c_analyse_targets) $(h_analyse_targets) 68 | 69 | $(c_analyse_targets): %-analyse: 70 | # -W options here are not clang compatible, so out of generic CFLAGS 71 | gcc $< -o /dev/null -c \ 72 | -std=gnu99 -Ofast -fwhole-program -Wall -Wextra \ 73 | -Wlogical-op -Wduplicated-cond \ 74 | -fanalyzer $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) 75 | clang $< -o /dev/null -c -std=gnu99 -Ofast -Weverything \ 76 | -Wno-documentation-unknown-command \ 77 | -Wno-language-extension-token \ 78 | -Wno-disabled-macro-expansion \ 79 | -Wno-padded \ 80 | -Wno-covered-switch-default \ 81 | -Wno-gnu-zero-variadic-macro-arguments \ 82 | -Wno-declaration-after-statement \ 83 | -Wno-cast-qual \ 84 | -Wno-unused-command-line-argument \ 85 | $(extra_clang_flags) \ 86 | $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) 87 | $(MAKE) $*-shared-analyse 88 | 89 | $(h_analyse_targets): %-analyse: 90 | $(MAKE) $*-shared-analyse 91 | 92 | %-shared-analyse: % 93 | # cppcheck is a bit dim about unused functions/variables, leave that to 94 | # clang/GCC 95 | cppcheck $< --std=c99 --quiet --inline-suppr --force \ 96 | --enable=all --suppress=missingIncludeSystem \ 97 | --suppress=unusedFunction --suppress=unmatchedSuppression \ 98 | --suppress=unreadVariable \ 99 | --suppress=checkersReport \ 100 | --suppress=normalCheckLevelMaxBranches \ 101 | --suppress=unusedStructMember \ 102 | --max-ctu-depth=32 --error-exitcode=1 103 | # clang-analyzer-unix.Malloc does not understand _drop_() 104 | clang-tidy $< --quiet -checks=-clang-analyzer-unix.Malloc -- -std=gnu99 105 | clang-format --dry-run --Werror $< 106 | 107 | tests: tests/test_store 108 | tests/test_store 109 | 110 | integration_tests: 111 | tests/x_integration_tests 112 | 113 | tests/test_store: tests/test_store.c src/store.o src/util.o 114 | $(CC) $(CFLAGS) $(CPPFLAGS) -I./src -o $@ $^ $(LDLIBS) 115 | 116 | .PHONY: all debug install uninstall clean analyse tests integration_tests 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | clipmenu is a simple clipboard manager using [dmenu][], [rofi][] or similar. 2 | 3 | # Demo 4 | 5 | ![Demo](https://cloud.githubusercontent.com/assets/660663/24079784/6f76da94-0c88-11e7-8251-40b1f02ebf3c.gif) 6 | 7 | # Usage 8 | 9 | ## clipmenud 10 | 11 | Start `clipmenud`, then run `clipmenu` to select something to put on the 12 | clipboard. For systemd users, a user service called `clipmenud` is packaged as 13 | part of the project. 14 | 15 | For those using a systemd unit and not using a desktop environment which does 16 | it automatically, you must import `$DISPLAY` so that `clipmenud` knows which X 17 | server to use. For example, in your `~/.xinitrc` do this prior to launching 18 | clipmenud: 19 | 20 | systemctl --user import-environment DISPLAY 21 | 22 | ## clipmenu 23 | 24 | You may wish to bind a shortcut in your window manager to launch `clipmenu`. 25 | 26 | All args passed to clipmenu are transparently dispatched to dmenu. That is, if 27 | you usually call dmenu with args to set colours and other properties, you can 28 | invoke clipmenu in exactly the same way to get the same effect, like so: 29 | 30 | clipmenu -i -fn Terminus:size=8 -nb '#002b36' -nf '#839496' -sb '#073642' -sf '#93a1a1' 31 | 32 | For a full list of environment variables that clipmenud can take, please see 33 | `man clipmenud`. 34 | 35 | There is also `clipdel` to delete clips, and `clipctl` to enable or disable 36 | clipboard monitoring. 37 | 38 | # Features 39 | 40 | The behavior of `clipmenud` can be customized through a config file. As some 41 | examples of things you can change: 42 | 43 | * Customising the maximum number of clips stored (default 1000) 44 | * Disabling clip collection temporarily with `clipctl disable`, reenabling with 45 | `clipctl enable` 46 | * Not storing clipboard changes from certain applications, like password 47 | managers 48 | * Taking direct ownership of the clipboard 49 | * ...and much more. 50 | 51 | See `man clipmenu.conf` to view all possible configuration variables and what 52 | they do. 53 | 54 | # Supported launchers 55 | 56 | Any dmenu-compliant application will work, but here are `CM_LAUNCHER` 57 | configurations that are known to work: 58 | 59 | - `dmenu` (the default) 60 | - `fzf` 61 | - `rofi` 62 | 63 | # Installation 64 | 65 | Several distributions, including Arch and Nix, provide clipmenu as an official 66 | package called `clipmenu`. 67 | 68 | ## Manual installation 69 | 70 | If your distribution doesn't provide a package, you can manually install using 71 | `make install` (or better yet, create a package for your distribution!). 72 | 73 | # How does it work? 74 | 75 | ## clipmenud 76 | 77 | 1. clipmenud passively monitors X11 clipboard selections (PRIMARY, CLIPBOARD, 78 | and SECONDARY) for changes using XFixes (no polling). 79 | 2. If `clipmenud` detects changes to the clipboard contents, it writes them out 80 | to storage and indexes using a hash as the filename. 81 | 82 | ## clipmenu 83 | 84 | 1. `clipmenu` reads the index to find all available clips. 85 | 2. `dmenu` (or another configured launcher) is executed to allow the user to 86 | select a clip. 87 | 3. After selection, the clip is put onto the PRIMARY and CLIPBOARD X 88 | selections. 89 | 90 | [dmenu]: http://tools.suckless.org/dmenu/ 91 | [rofi]: https://github.com/DaveDavenport/Rofi 92 | -------------------------------------------------------------------------------- /init/clipmenud.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Clipmenu daemon 3 | 4 | [Service] 5 | ExecStart=@bindir@/clipmenud 6 | Restart=always 7 | RestartSec=500ms 8 | 9 | MemoryDenyWriteExecute=yes 10 | NoNewPrivileges=yes 11 | ProtectControlGroups=yes 12 | ProtectKernelTunables=yes 13 | RestrictAddressFamilies= 14 | RestrictRealtime=yes 15 | 16 | # We don't need to do any clean up, so if something hangs (borked X server, 17 | # etc), it's going to stay that way. Just forcefully kill and get it over with. 18 | TimeoutStopSec=2 19 | 20 | [Install] 21 | WantedBy=default.target 22 | -------------------------------------------------------------------------------- /man/clipctl.1: -------------------------------------------------------------------------------- 1 | .TH CLIPCTL 1 2 | .SH NAME 3 | clipctl \- control the clipmenud daemon 4 | .SH SYNOPSIS 5 | .B clipctl 6 | ACTION 7 | .SH DESCRIPTION 8 | .B clipctl 9 | communicates with the clipmenud daemon to control clipboard collection. It 10 | sends a signal to clipmenud so that clipboard monitoring is enabled, disabled, 11 | or toggled, or it simply displays the current status. 12 | .SH OPTIONS 13 | .TP 14 | .B enable 15 | Instruct clipmenud to enable clipboard collection. 16 | .TP 17 | .B disable 18 | Instruct clipmenud to disable clipboard collection. 19 | .TP 20 | .B toggle 21 | Invert the current state of clipboard collection. 22 | .TP 23 | .B status 24 | Print the current state of clipboard collection. 25 | 26 | .TP 27 | clipctl also accepts one dash option: 28 | 29 | .B \-h, \--help 30 | Display the help message (invokes the manual page). 31 | .SH CONFIGURATION 32 | See 33 | .BR clipmenu.conf (5). 34 | .SH DEPENDENCIES 35 | clipctl requires a running instance of the 36 | .BR clipmenud 37 | daemon. 38 | .SH SEE ALSO 39 | .BR clipdel (1), 40 | .BR clipmenu (1), 41 | .BR clipmenud (1), 42 | .BR clipmenu.conf (5) 43 | .SH AUTHOR 44 | Chris Down 45 | .MT chris@chrisdown.name 46 | .ME 47 | .SH REPORTING BUGS 48 | Please send bug reports to 49 | .UR https://github.com/cdown/clipmenu/issues 50 | .UE . 51 | -------------------------------------------------------------------------------- /man/clipdel.1: -------------------------------------------------------------------------------- 1 | .TH CLIPDEL 1 2 | .SH NAME 3 | clipdel \- delete entries from the clip store 4 | .SH SYNOPSIS 5 | .B clipdel 6 | [OPTION...] PATTERN 7 | .SH DESCRIPTION 8 | .B clipdel 9 | removes clipboard entries from the clip store managed by clipmenu. By default, 10 | it performs a dry-run and prints any matching entries. With the -d flag, 11 | matching entries are permanently removed. 12 | .SH OPTIONS 13 | .TP 14 | .B \-d 15 | Real deletion mode. Matching clipboard entries will be removed. 16 | .TP 17 | .B \-F 18 | Perform a literal (fixed-string) match instead of interpreting the pattern as a regular expression. 19 | .TP 20 | .B \-N 21 | Interpret PATTERN as the amount of entries to delete starting from the oldest. 22 | E.g 23 | .I "\-N 1" 24 | deletes the oldest entry. 25 | .TP 26 | .B \-n 27 | Same as 28 | .B \-N 29 | but starting from newest. 30 | .TP 31 | .B \-v 32 | Invert the matching condition; entries that do not match the given pattern are selected for deletion. 33 | .TP 34 | .B \-h, \--help 35 | Display the help message (invokes the manual page). 36 | .SH CONFIGURATION 37 | See 38 | .BR clipmenu.conf (5). 39 | .SH DEPENDENCIES 40 | clipdel requires access to the clip store directory as defined in the configuration. 41 | .SH SEE ALSO 42 | .BR clipctl (1), 43 | .BR clipmenu (1), 44 | .BR clipmenud (1), 45 | .BR clipmenu.conf (5) 46 | .SH AUTHOR 47 | Chris Down 48 | .MT chris@chrisdown.name 49 | .ME 50 | .SH REPORTING BUGS 51 | Please send bug reports to 52 | .UR https://github.com/cdown/clipmenu/issues 53 | .UE . 54 | -------------------------------------------------------------------------------- /man/clipmenu.1: -------------------------------------------------------------------------------- 1 | .TH CLIPMENU 1 2 | .SH NAME 3 | clipmenu \- interactive launcher for clipboard entry selection 4 | .SH SYNOPSIS 5 | .B clipmenu 6 | [OPTION...] 7 | .SH DESCRIPTION 8 | .B clipmenu 9 | provides an interactive interface for browsing and selecting clipboard entries 10 | stored by the clipmenud. It launches a menu using a configured launcher 11 | (e.g., dmenu, rofi, or another custom command) and displays a numbered list of stored 12 | clips. Once a selection is made, clipmenu calls 13 | .BR clipserve 14 | to set the chosen clip as the current X11 clipboard. 15 | .SH OPTIONS 16 | .TP 17 | .B \-h, \--help 18 | Display the help message (invokes the manual page). 19 | 20 | Other than that, command-line arguments are passed directly to the underlying 21 | launcher. 22 | .SH CONFIGURATION 23 | See 24 | .BR clipmenu.conf (5). 25 | .SH DEPENDENCIES 26 | clipmenu requires a launcher capable of a dmenu-like interface (e.g., rofi or a 27 | custom command). Population of the clipstore requires 28 | .BR clipmenud 29 | to be running. 30 | .SH SEE ALSO 31 | .BR clipctl (1), 32 | .BR clipdel (1), 33 | .BR clipmenud (1), 34 | .BR clipmenu.conf (5) 35 | .SH AUTHOR 36 | Chris Down 37 | .MT chris@chrisdown.name 38 | .ME 39 | .SH REPORTING BUGS 40 | Please send bug reports to 41 | .UR https://github.com/cdown/clipmenu/issues 42 | .UE . 43 | -------------------------------------------------------------------------------- /man/clipmenu.conf.5: -------------------------------------------------------------------------------- 1 | .TH CLIPMENU.CONF 5 2 | .SH NAME 3 | clipmenu.conf \- configuration file for clipmenu applications 4 | .SH DESCRIPTION 5 | clipmenu.conf defines the runtime settings for clipmenu applications (including 6 | clipmenu itself, clipmenud, clipctl, clipdel, and clipserve). 7 | .SH FORMAT 8 | Each non-comment line in clipmenu.conf consists of a key followed by one or 9 | more whitespace-separated values. Blank lines and lines starting with '#' are 10 | ignored. 11 | .SH CONFIGURATION OPTIONS 12 | .TP 13 | .B max_clips 14 | Specifies the maximum number of clipboard entries to retain in the clip store. 15 | Default: 1000. 16 | .TP 17 | .B max_clips_batch 18 | Provides a buffer above max_clips; when the number of entries exceeds 19 | (max_clips + max_clips_batch), the clip store is trimmed back to max_clips 20 | entries. Default: 100. 21 | .TP 22 | .B oneshot 23 | If set to 1, clipmenud processes clipboard selections only once before exiting. 24 | Default: 0. 25 | .TP 26 | .B own_clipboard 27 | Determines whether clipmenud should claim ownership of the X11 clipboard. Works 28 | together with own_selections. Default: 0. 29 | .TP 30 | .B own_selections 31 | Specifies which X11 selections (e.g., "clipboard" or "primary") clipmenud 32 | should actively own. Default: "clipboard". 33 | .TP 34 | .B selections 35 | Lists the X11 selections to monitor for changes. Valid values include 36 | "clipboard", "primary", and "secondary". Default: "clipboard primary". 37 | .TP 38 | .B ignore_window 39 | Defines a regular expression matching window titles to exclude from clipboard 40 | monitoring. Unset by default. 41 | .TP 42 | .B launcher 43 | Specifies the launcher command to use with clipmenu. Alternative choices 44 | include rofi's dmenu mode or a custom command. Default: "dmenu". 45 | .TP 46 | .B launcher_pass_dmenu_args 47 | When enabled, extra command-line arguments passed to clipmenu are forwarded to 48 | the launcher. Default: 1. 49 | .TP 50 | .B touch_on_select 51 | When an entry is selected via 52 | .BR clipmenu (1) 53 | it will also be moved up to the newest slot. 54 | Default: 0. 55 | .TP 56 | .B cm_dir 57 | Overrides the default directory for the clip store. This is by default at a 58 | subdirectory inside XDG_RUNTIME_DIR, TMPDIR, or if both are unset, inside /tmp. 59 | .SH FILE LOCATION 60 | Typically, clipmenu.conf is located at 61 | .BR ~/.config/clipmenu/clipmenu.conf 62 | . 63 | .SH SEE ALSO 64 | .BR clipctl (1), 65 | .BR clipdel (1), 66 | .BR clipmenu (1), 67 | .BR clipmenud (1), 68 | .SH AUTHOR 69 | Chris Down 70 | .MT chris@chrisdown.name 71 | .ME 72 | .SH REPORTING BUGS 73 | Please send bug reports to 74 | .UR https://github.com/cdown/clipmenu/issues 75 | .UE . 76 | -------------------------------------------------------------------------------- /man/clipmenud.1: -------------------------------------------------------------------------------- 1 | .TH CLIPMENUD 1 2 | .SH NAME 3 | clipmenud \- daemon for clipboard monitoring and storage 4 | .SH DESCRIPTION 5 | .B clipmenud 6 | runs in the background, monitoring X11 clipboard selections (including PRIMARY, 7 | CLIPBOARD, and SECONDARY). It stores new clipboard entries into a persistent 8 | clip store. clipmenud responds to signals sent by 9 | .BR clipctl 10 | to enable or disable clipboard collection. 11 | .SH OPTIONS 12 | .TP 13 | .B \-h, \--help 14 | Display the help message (invokes the manual page). 15 | .SH CONFIGURATION 16 | See 17 | .BR clipmenu.conf (5). 18 | .SH DEPENDENCIES 19 | clipmenud requires an X11 environment with the XFixes extension and access to the clip store directory as defined in the configuration. 20 | .SH SEE ALSO 21 | .BR clipctl (1), 22 | .BR clipdel (1), 23 | .BR clipmenu (1), 24 | .BR clipmenu.conf (5) 25 | .SH AUTHOR 26 | Chris Down 27 | .MT chris@chrisdown.name 28 | .ME 29 | .SH REPORTING BUGS 30 | Please send bug reports to 31 | .UR https://github.com/cdown/clipmenu/issues 32 | .UE . 33 | -------------------------------------------------------------------------------- /man/clipserve.1: -------------------------------------------------------------------------------- 1 | .TH CLIPSERVE 1 2 | .SH NAME 3 | clipserve \- serve a selected clipboard entry to X11 selections 4 | .SH SYNOPSIS 5 | .B clipserve 6 | 7 | .SH DESCRIPTION 8 | .B clipserve 9 | serves the clipboard content identified by a hash from the clip store on the 10 | X11 clipboard. 11 | 12 | This program is not usually invoked directly, but is instead called from inside 13 | other clipmenu applications. 14 | .SH OPTIONS 15 | .TP 16 | .B \-h, \--help 17 | Display the help message (invokes the manual page). 18 | .TP 19 | clipserve requires exactly one argument, which is the hash of the clipboard 20 | entry to be served. 21 | .SH CONFIGURATION 22 | See 23 | .BR clipmenu.conf (5). 24 | .SH DEPENDENCIES 25 | clipserve needs a valid clip store and an operational X11 environment to 26 | function correctly. 27 | .SH SEE ALSO 28 | .BR clipctl (1), 29 | .BR clipdel (1), 30 | .BR clipmenu (1), 31 | .BR clipmenud (1), 32 | .BR clipmenu.conf (5) 33 | .SH AUTHOR 34 | Chris Down 35 | .MT chris@chrisdown.name 36 | .ME 37 | .SH REPORTING BUGS 38 | Please send bug reports to 39 | .UR https://github.com/cdown/clipmenu/issues 40 | .UE . 41 | -------------------------------------------------------------------------------- /src/clipctl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.h" 14 | #include "util.h" 15 | 16 | /** 17 | * Check if the clipmenud service is enabled. 18 | */ 19 | static bool is_enabled(struct config *cfg) { 20 | _drop_(fclose) FILE *file = fopen(get_enabled_path(cfg), "r"); 21 | die_on(!file, "Failed to open enabled file: %s\n", strerror(errno)); 22 | return fgetc(file) == '1'; 23 | } 24 | 25 | /** 26 | * Retrieve the process ID of the clipmenud daemon. 27 | * 28 | * -EEXIST is returned if multiple instances are detected. -ENOENT is returned 29 | * if no instances are found. 30 | */ 31 | static pid_t get_clipmenud_pid(void) { 32 | _drop_(closedir) DIR *dir = opendir("/proc"); 33 | die_on(!dir, "Support without /proc is not implemented yet\n"); 34 | 35 | pid_t ret = 0; 36 | struct dirent *ent; 37 | 38 | while ((ent = readdir(dir)) && ret >= 0) { 39 | uint64_t pid; 40 | if (str_to_uint64(ent->d_name, &pid) < 0) { 41 | continue; 42 | } 43 | char buf[PATH_MAX]; 44 | snprintf_safe(buf, sizeof(buf), "/proc/%s/comm", ent->d_name); 45 | _drop_(fclose) FILE *fp = fopen(buf, "r"); 46 | if (fp && fgets(buf, sizeof(buf), fp) && streq(buf, "clipmenud\n")) { 47 | ret = ret ? -EEXIST : (pid_t)pid; 48 | } 49 | } 50 | 51 | return ret ? ret : -ENOENT; 52 | } 53 | 54 | /** 55 | * Determine if clipmenud should be enabled based on the given mode string and 56 | * clipmenu state. 57 | */ 58 | static bool _nonnull_ should_enable(struct config *cfg, const char *mode_str) { 59 | if (streq(mode_str, "enable")) { 60 | return true; 61 | } else if (streq(mode_str, "disable")) { 62 | return false; 63 | } else if (streq(mode_str, "toggle")) { 64 | return !is_enabled(cfg); 65 | } 66 | 67 | die("Unknown command: %s\n", mode_str); 68 | } 69 | 70 | int main(int argc, char *argv[]) { 71 | _drop_(config_free) struct config cfg = setup("clipctl"); 72 | exec_man_on_help(argc, argv); 73 | die_on(argc != 2, "Usage: clipctl \n"); 74 | 75 | pid_t pid = get_clipmenud_pid(); 76 | die_on(pid == -ENOENT, "clipmenud is not running\n"); 77 | die_on(pid == -EEXIST, "Multiple instances of clipmenud are running\n"); 78 | expect(pid > 0); 79 | 80 | if (streq(argv[1], "status")) { 81 | printf("%s\n", is_enabled(&cfg) ? "enabled" : "disabled"); 82 | return 0; 83 | } 84 | 85 | bool want_enable = should_enable(&cfg, argv[1]); 86 | 87 | expect(kill(pid, want_enable ? SIGUSR2 : SIGUSR1) == 0); 88 | dbg("Sent signal to pid %d\n", pid); 89 | 90 | unsigned int delay_ms = 1; 91 | unsigned int total_wait_ms = 0; 92 | const unsigned int max_wait_ms = 1000; 93 | 94 | while (total_wait_ms < max_wait_ms) { 95 | if (is_enabled(&cfg) == want_enable) { 96 | return 0; 97 | } 98 | 99 | struct timespec req = {.tv_sec = delay_ms / 1000, 100 | .tv_nsec = (delay_ms % 1000) * 1000000L}; 101 | struct timespec rem; 102 | 103 | while (nanosleep(&req, &rem) != 0) { 104 | expect(errno == EINTR); 105 | req = rem; 106 | } 107 | 108 | total_wait_ms += delay_ms; 109 | delay_ms *= 2; 110 | } 111 | 112 | die("Failed to %s clipmenud within %u ms\n", 113 | want_enable ? "enable" : "disable", max_wait_ms); 114 | } 115 | -------------------------------------------------------------------------------- /src/clipdel.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.h" 10 | #include "store.h" 11 | #include "util.h" 12 | 13 | /** 14 | * The deletion mode for clipdel operations. 15 | */ 16 | enum delete_mode { 17 | DELETE_DRY_RUN, 18 | DELETE_REAL, 19 | }; 20 | 21 | /** 22 | * The match type for clipdel operations. 23 | */ 24 | enum match_type { 25 | MATCH_REGEX, 26 | MATCH_LITERAL, 27 | MATCH_COUNT_NEWEST, 28 | MATCH_COUNT_OLDEST, 29 | }; 30 | 31 | /** 32 | * Holds the application state for a clipdel operation in preparation for 33 | * passing it as private data to the cs_remove callback. 34 | */ 35 | struct clipdel_state { 36 | enum delete_mode mode; 37 | enum match_type type; 38 | bool invert_match; 39 | union { 40 | regex_t rgx; 41 | const char *needle; 42 | struct { 43 | uint64_t current; 44 | uint64_t num_delete; 45 | } count; 46 | }; 47 | }; 48 | 49 | /** 50 | * Callback for cs_remove. In order for the delete to actually happen, we must 51 | * be running DELETE_REAL. 52 | */ 53 | static enum cs_remove_action _nonnull_ remove_if_match(uint64_t hash _unused_, 54 | const char *line, 55 | void *private) { 56 | struct clipdel_state *state = private; 57 | int ret; 58 | bool matches; 59 | switch (state->type) { 60 | case MATCH_LITERAL: 61 | matches = strstr(line, state->needle) != NULL; 62 | break; 63 | case MATCH_REGEX: 64 | ret = regexec(&state->rgx, line, 0, NULL, 0); 65 | expect(ret == 0 || ret == REG_NOMATCH); 66 | matches = ret == 0; 67 | break; 68 | case MATCH_COUNT_NEWEST: 69 | case MATCH_COUNT_OLDEST: 70 | if (state->count.current < state->count.num_delete) { 71 | matches = true; 72 | ++state->count.current; 73 | } else { 74 | matches = false; 75 | } 76 | break; 77 | default: 78 | die("unreachable\n"); 79 | } 80 | 81 | bool wants_del = state->invert_match ? !matches : matches; 82 | if (wants_del) { 83 | puts(line); 84 | } 85 | 86 | return state->mode == DELETE_REAL && wants_del ? CS_ACTION_REMOVE 87 | : CS_ACTION_KEEP; 88 | } 89 | 90 | int main(int argc, char *argv[]) { 91 | const char usage[] = "Usage: clipdel [-d] [-F] [-N] [-n] [-v] pattern"; 92 | 93 | _drop_(config_free) struct config cfg = setup("clipdel"); 94 | 95 | struct clipdel_state state = {0}; 96 | 97 | int opt; 98 | while ((opt = getopt(argc, argv, "dFNnvh")) != -1) { 99 | switch (opt) { 100 | case 'd': 101 | state.mode = DELETE_REAL; 102 | break; 103 | case 'F': 104 | state.type = MATCH_LITERAL; 105 | break; 106 | case 'N': 107 | state.type = MATCH_COUNT_OLDEST; 108 | break; 109 | case 'n': 110 | state.type = MATCH_COUNT_NEWEST; 111 | break; 112 | case 'v': 113 | state.invert_match = true; 114 | break; 115 | case 'h': 116 | exec_man(); 117 | break; 118 | default: 119 | die("%s\n", usage); 120 | } 121 | } 122 | 123 | die_on(optind >= argc, "%s\n", usage); 124 | 125 | _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); 126 | _drop_(close) int snip_fd = 127 | open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); 128 | expect(content_dir_fd >= 0 && snip_fd >= 0); 129 | 130 | _drop_(cs_destroy) struct clip_store cs; 131 | expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); 132 | 133 | enum cs_iter_direction direction = CS_ITER_OLDEST_FIRST; 134 | switch (state.type) { 135 | case MATCH_REGEX: 136 | die_on(regcomp(&state.rgx, argv[optind], REG_EXTENDED | REG_NOSUB), 137 | "Could not compile regex\n"); 138 | break; 139 | case MATCH_LITERAL: 140 | state.needle = argv[optind]; 141 | break; 142 | case MATCH_COUNT_NEWEST: 143 | case MATCH_COUNT_OLDEST: 144 | die_on(str_to_uint64(argv[optind], &state.count.num_delete) < 0, 145 | "Bad argument, expected integer: %s\n", argv[optind]); 146 | state.count.current = 0; 147 | if (state.type == MATCH_COUNT_NEWEST) { 148 | direction = CS_ITER_NEWEST_FIRST; 149 | } 150 | break; 151 | default: 152 | die("unreachable\n"); 153 | } 154 | 155 | expect(cs_remove(&cs, direction, remove_if_match, &state) == 0); 156 | 157 | if (state.type == MATCH_REGEX) { 158 | regfree(&state.rgx); 159 | } 160 | 161 | return 0; 162 | } 163 | -------------------------------------------------------------------------------- /src/clipmenu.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "config.h" 11 | #include "store.h" 12 | #include "util.h" 13 | 14 | #define MAX_ARGS 32 15 | 16 | static int dmenu_user_argc; 17 | static char **dmenu_user_argv; 18 | 19 | /** 20 | * Calculate the base 10 padding length for a number. 21 | */ 22 | static int get_padding_length(size_t num) { 23 | int digits = 1; 24 | while (num /= 10) { 25 | digits++; 26 | } 27 | return digits; 28 | } 29 | 30 | /** 31 | * Execute the launcher. Called after fork() is already done in the new child. 32 | */ 33 | static void _noreturn_ _nonnull_ exec_launcher(struct config *cfg, 34 | int *input_pipe, 35 | int *output_pipe) { 36 | dup2(input_pipe[0], STDIN_FILENO); 37 | close(input_pipe[1]); 38 | close(output_pipe[0]); 39 | dup2(output_pipe[1], STDOUT_FILENO); 40 | 41 | const char *const dmenu_args[] = {"-p", "clipmenu", "-l", "20"}; 42 | const char **cmd = malloc(MAX_ARGS * sizeof(char *)); 43 | 44 | size_t d_i = 0; 45 | 46 | switch (cfg->launcher.ltype) { 47 | case LAUNCHER_ROFI: 48 | cmd[d_i++] = "rofi"; 49 | cmd[d_i++] = "--"; 50 | cmd[d_i++] = "-dmenu"; 51 | break; 52 | case LAUNCHER_CUSTOM: 53 | cmd[d_i++] = cfg->launcher.custom; 54 | break; 55 | default: 56 | die("Unreachable\n"); 57 | } 58 | 59 | if (cfg->launcher_pass_dmenu_args) { 60 | expect(d_i + arrlen(dmenu_args) < MAX_ARGS); 61 | for (size_t i = 0; i < arrlen(dmenu_args); i++) { 62 | cmd[d_i++] = dmenu_args[i]; 63 | } 64 | } 65 | 66 | for (int i = 1; i < dmenu_user_argc && d_i < MAX_ARGS - 1; i++, d_i++) { 67 | cmd[d_i] = dmenu_user_argv[i]; 68 | } 69 | 70 | cmd[d_i] = NULL; 71 | execvp(cmd[0], (char *const *)cmd); // SUS says cmd unchanged 72 | die("Failed to exec %s: %s\n", cmd[0], strerror(errno)); 73 | } 74 | 75 | static int dprintf_ellipsise_long_snip_line(int fd, const char *line) { 76 | size_t line_len = strlen(line); 77 | if (line_len == CS_SNIP_LINE_SIZE - 1) { 78 | return dprintf(fd, "%.*s...", (int)(CS_SNIP_LINE_SIZE - 4), line); 79 | } else { 80 | return dprintf(fd, "%s", line); 81 | } 82 | } 83 | 84 | /** 85 | * Writes the available clips to the launcher and reads back the user's 86 | * selection. 87 | */ 88 | static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe, 89 | int *output_pipe, uint64_t *out_hash) { 90 | close(input_pipe[0]); 91 | close(output_pipe[1]); 92 | 93 | _drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY); 94 | _drop_(close) int snip_fd = 95 | open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600); 96 | expect(content_dir_fd >= 0 && snip_fd >= 0); 97 | 98 | _drop_(cs_destroy) struct clip_store cs; 99 | expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); 100 | 101 | struct ref_guard guard = cs_ref(&cs); 102 | size_t cur_clips; 103 | expect(cs_len(&cs, &cur_clips) == 0); 104 | _drop_(free) uint64_t *idx_to_hash = malloc(cur_clips * sizeof(uint64_t)); 105 | expect(idx_to_hash); 106 | int pad = get_padding_length(cur_clips); 107 | size_t clip_idx = cur_clips; 108 | 109 | struct cs_snip *snip = NULL; 110 | while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { 111 | expect(dprintf(input_pipe[1], "[%*zu] ", pad, clip_idx--) > 0); 112 | expect(dprintf_ellipsise_long_snip_line(input_pipe[1], snip->line) > 0); 113 | if (snip->nr_lines > 1) { 114 | expect(dprintf(input_pipe[1], " (%zu lines)", snip->nr_lines) > 0); 115 | } 116 | write_safe(input_pipe[1], "\n", 1); 117 | idx_to_hash[clip_idx] = snip->hash; 118 | } 119 | 120 | // We've written everything and have our own map, no need to hold any more 121 | cs_unref(guard.cs); 122 | 123 | close(input_pipe[1]); 124 | 125 | char sel_idx_str[UINT64_MAX_STRLEN + 1]; 126 | read_safe(output_pipe[0], sel_idx_str, 1); // Discard the leading "[" 127 | size_t read_sz = read_safe(output_pipe[0], sel_idx_str, UINT64_MAX_STRLEN); 128 | sel_idx_str[read_sz] = '\0'; 129 | char *end_ptr = strchr(sel_idx_str, ']'); 130 | if (end_ptr) { 131 | *end_ptr = '\0'; 132 | } 133 | 134 | uint64_t sel_idx; 135 | int forced_ret = 0; 136 | if (str_to_uint64(sel_idx_str, &sel_idx) < 0 || sel_idx == 0 || 137 | sel_idx > cur_clips) { 138 | forced_ret = EXIT_FAILURE; 139 | } else { 140 | *out_hash = idx_to_hash[sel_idx - 1]; 141 | } 142 | 143 | int dmenu_status; 144 | while (wait(&dmenu_status) < 0 && errno == EINTR) 145 | ; 146 | close(output_pipe[0]); 147 | 148 | if (forced_ret || !WIFEXITED(dmenu_status)) { 149 | return EXIT_FAILURE; 150 | } 151 | 152 | int dmenu_exit_code = WEXITSTATUS(dmenu_status); 153 | if (dmenu_exit_code == EXIT_SUCCESS && cfg->touch_on_select) { 154 | expect(cs_make_newest(&cs, *out_hash) == 0); 155 | } 156 | return dmenu_exit_code; 157 | } 158 | 159 | /** 160 | * Prompts the user to select a clip via their launcher, and returns the 161 | * selected content hash. 162 | */ 163 | static int _nonnull_ prompt_user_for_hash(struct config *cfg, uint64_t *hash) { 164 | int input_pipe[2], output_pipe[2]; 165 | expect(pipe(input_pipe) == 0 && pipe(output_pipe) == 0); 166 | 167 | pid_t pid = fork(); 168 | expect(pid >= 0); 169 | 170 | if (pid == 0) { 171 | exec_launcher(cfg, input_pipe, output_pipe); 172 | } 173 | 174 | return interact_with_dmenu(cfg, input_pipe, output_pipe, hash); 175 | } 176 | 177 | int main(int argc, char *argv[]) { 178 | dmenu_user_argc = argc; 179 | dmenu_user_argv = argv; 180 | 181 | _drop_(config_free) struct config cfg = setup("clipmenu"); 182 | exec_man_on_help(argc, argv); 183 | 184 | uint64_t hash; 185 | int dmenu_exit_code = prompt_user_for_hash(&cfg, &hash); 186 | 187 | if (dmenu_exit_code == EXIT_SUCCESS) { 188 | run_clipserve(hash); 189 | } 190 | 191 | return dmenu_exit_code; 192 | } 193 | -------------------------------------------------------------------------------- /src/clipmenud.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "config.h" 20 | #include "store.h" 21 | #include "util.h" 22 | #include "x.h" 23 | 24 | static Display *dpy; 25 | static struct clip_store cs; 26 | static struct config cfg; 27 | static Window win; 28 | 29 | static int enabled = 1; 30 | static int sig_fd; 31 | 32 | static Atom timestamp_atom; 33 | static Atom incr_atom; 34 | static struct incr_transfer *it_list; 35 | 36 | static struct cm_selections sels[CM_SEL_MAX]; 37 | 38 | static Time last_disable_time = 0; 39 | static Time last_enable_time = 0; 40 | 41 | enum clip_text_source { 42 | CLIP_TEXT_SOURCE_X, 43 | CLIP_TEXT_SOURCE_MALLOC, 44 | CLIP_TEXT_SOURCE_INVALID 45 | }; 46 | 47 | struct clip_text { 48 | char *data; 49 | enum clip_text_source source; 50 | }; 51 | 52 | static void free_clip_text(struct clip_text *ct) { 53 | expect(ct->source != CLIP_TEXT_SOURCE_INVALID); 54 | 55 | if (ct->data) { 56 | if (ct->source == CLIP_TEXT_SOURCE_X) { 57 | XFree(ct->data); 58 | } else { 59 | free(ct->data); 60 | } 61 | ct->data = NULL; 62 | } 63 | 64 | ct->source = CLIP_TEXT_SOURCE_INVALID; 65 | } 66 | 67 | /** 68 | * Get the current X server time by triggering a PropertyNotify. 69 | */ 70 | static Time get_current_server_time(void) { 71 | XEvent ev; 72 | XChangeProperty(dpy, win, timestamp_atom, XA_INTEGER, 32, PropModeReplace, 73 | NULL, 0); 74 | XSync(dpy, False); 75 | 76 | while (1) { 77 | XNextEvent(dpy, &ev); 78 | if (ev.type == PropertyNotify && ev.xproperty.atom == timestamp_atom) { 79 | XDeleteProperty(dpy, win, timestamp_atom); 80 | return ev.xproperty.time; 81 | } 82 | XPutBackEvent(dpy, &ev); 83 | } 84 | } 85 | 86 | /** 87 | * Check if a text s1 is a possible partial of s2. 88 | * 89 | * Chromium and some other badly behaved applications spam PRIMARY during 90 | * selection, so if you're selecting the text "abc", you get three clips: "a", 91 | * "ab", and "abc" (or "c", "bc", "abc" if selecting right to left). Attempt to 92 | * detect these. It's possible we were not fast enough to get all of them, so 93 | * unfortunately we can't check for strlen(s1)+1 either. It's also possible the 94 | * user first expands, and then retracts the selection, so we need to handle 95 | * that too. 96 | */ 97 | static bool is_possible_partial(const char *s1, const char *s2) { 98 | size_t len1 = strlen(s1), len2 = strlen(s2); 99 | 100 | // Is one a prefix of the other? 101 | if (strncmp(s1, s2, len1 < len2 ? len1 : len2) == 0) { 102 | return true; 103 | } 104 | 105 | // Is one a suffix of the other? 106 | if (len1 < len2) { 107 | return strcmp(s1, s2 + len2 - len1) == 0; 108 | } else { 109 | return strcmp(s2, s1 + len1 - len2) == 0; 110 | } 111 | } 112 | 113 | /** 114 | * Retrieve the converted text put into our clip atom. In order for this to 115 | * happen a conversion must have been performed in an earlier iteration with 116 | * XConvertSelection. 117 | */ 118 | static struct clip_text get_clipboard_text(Atom clip_atom) { 119 | struct clip_text ct = {NULL, CLIP_TEXT_SOURCE_X}; 120 | unsigned char *cur_text; 121 | Atom actual_type; 122 | int actual_format; 123 | unsigned long nitems, bytes_after; 124 | 125 | int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False, 126 | AnyPropertyType, &actual_type, &actual_format, 127 | &nitems, &bytes_after, &cur_text); 128 | if (res != Success) { 129 | return ct; 130 | } 131 | 132 | if (actual_type == incr_atom) { 133 | dbg("Unexpected INCR transfer detected\n"); 134 | XFree(cur_text); 135 | return ct; 136 | } 137 | 138 | ct.data = (char *)cur_text; 139 | 140 | return ct; 141 | } 142 | 143 | /** 144 | * Return true if the given string contains any non-whitespace characters. 145 | */ 146 | static bool is_salient_text(const char *str) { 147 | if (!str) { 148 | return false; 149 | } 150 | 151 | for (; *str; str++) { 152 | if (!isspace((unsigned char)*str)) { 153 | return true; 154 | } 155 | } 156 | return false; 157 | } 158 | 159 | /** 160 | * Write the current enabled status to a designated status file. 161 | */ 162 | static void write_status(void) { 163 | _drop_(close) int fd = 164 | open(get_enabled_path(&cfg), O_WRONLY | O_CREAT, 0600); 165 | die_on(fd < 0, "Failed to update status: %s\n", strerror(errno)); 166 | dprintf(fd, "%d", (int)enabled); 167 | } 168 | 169 | /** 170 | * Return true if the given window title matches the title of the clipserve 171 | * window. 172 | */ 173 | static bool is_clipserve(const char *win_title) { 174 | return win_title && streq(win_title, "clipserve"); 175 | } 176 | 177 | /** 178 | * Determine if a window with the given title should be ignored based on user 179 | * configuration. 180 | */ 181 | static bool is_ignored_window(char *win_title) { 182 | if (!win_title || !cfg.ignore_window.set) { 183 | return 0; 184 | } 185 | int ret = regexec(&cfg.ignore_window.rgx, win_title, 0, NULL, 0); 186 | expect(ret == 0 || ret == REG_NOMATCH); 187 | return !ret; 188 | } 189 | 190 | /** 191 | * Disable or enable clip collection based on received signals. 192 | */ 193 | static void handle_signalfd_event(void) { 194 | struct signalfd_siginfo si; 195 | ssize_t s = read(sig_fd, &si, sizeof(struct signalfd_siginfo)); 196 | expect(s == sizeof(struct signalfd_siginfo)); 197 | dbg("Got signal %" PRIu32 " from pid %" PRIu32 "\n", si.ssi_signo, 198 | si.ssi_pid); 199 | switch (si.ssi_signo) { 200 | case SIGUSR1: 201 | // If we're already disabled, we need to keep the original 202 | // timestamp to properly filter all events that were queued during 203 | // any part of the disabled period. 204 | if (enabled) { 205 | last_disable_time = get_current_server_time(); 206 | } 207 | enabled = 0; 208 | dbg("Clipboard collection disabled by signal at time %lu\n", 209 | (unsigned long)last_disable_time); 210 | break; 211 | case SIGUSR2: 212 | // If we're already enabled, we need to keep the original timestamp 213 | // so we don't mistakenly filter out valid messages. 214 | if (!enabled) { 215 | last_enable_time = get_current_server_time(); 216 | } 217 | enabled = 1; 218 | dbg("Clipboard collection enabled by signal at time %lu\n", 219 | (unsigned long)last_enable_time); 220 | break; 221 | } 222 | write_status(); 223 | } 224 | 225 | /** 226 | * Something changed about the watched selection, consider converting it to our 227 | * desired property type. 228 | */ 229 | static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) { 230 | if (last_disable_time > 0 && se->timestamp >= last_disable_time && 231 | se->timestamp < last_enable_time) { 232 | dbg("Ignoring selection event from disabled period (event time: %lu, disabled: %lu, enabled: %lu)\n", 233 | (unsigned long)se->timestamp, (unsigned long)last_disable_time, 234 | (unsigned long)last_enable_time); 235 | return; 236 | } 237 | 238 | enum selection_type sel = 239 | selection_atom_to_selection_type(se->selection, sels); 240 | if (sel == CM_SEL_INVALID) { 241 | dbg("Received XFixesSelectionNotify for unknown sel\n"); 242 | return; 243 | } 244 | 245 | _drop_(XFree) char *win_title = get_window_title(dpy, se->owner); 246 | if (is_clipserve(win_title) || is_ignored_window(win_title)) { 247 | dbg("Ignoring clip from window titled '%s'\n", win_title); 248 | return; 249 | } 250 | 251 | dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n", 252 | cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner); 253 | XConvertSelection(dpy, se->selection, 254 | XInternAtom(dpy, "UTF8_STRING", False), sels[sel].storage, 255 | win, CurrentTime); 256 | 257 | return; 258 | } 259 | 260 | /** 261 | * Something changed about the watched selection, but we don't explicitly 262 | * listen for SelectionNotify, so in reality this only happens in response to 263 | * an explicit request to tell us that there is no owner. In that case, return 264 | * -ENOENT. 265 | */ 266 | static int handle_selection_notify(const XSelectionEvent *se) { 267 | if (se->property == None) { 268 | enum selection_type sel = 269 | selection_atom_to_selection_type(se->selection, sels); 270 | if (sel == CM_SEL_INVALID) { 271 | dbg("Received no owner notification for unknown sel\n"); 272 | return 0; 273 | } 274 | dbg("X reports that %s has no current owner\n", 275 | cfg.selections[sel].name); 276 | return -ENOENT; 277 | } 278 | return 0; 279 | } 280 | 281 | /** 282 | * Trims the clip store if the number of clips exceeds the configured batch 283 | * size. 284 | */ 285 | static void maybe_trim(void) { 286 | size_t cur_clips; 287 | expect(cs_len(&cs, &cur_clips) == 0); 288 | if (cur_clips > (size_t)cfg.max_clips + (size_t)cfg.max_clips_batch) { 289 | expect(cs_trim(&cs, CS_ITER_NEWEST_FIRST, (size_t)cfg.max_clips) == 0); 290 | } 291 | } 292 | 293 | /** 294 | * Clips more than this many seconds apart are not considered for partial merge 295 | */ 296 | #define PARTIAL_MAX_SECS 2 297 | 298 | /** 299 | * Store the clipboard text. If the text is a possible partial of the last clip 300 | * and it was received shortly afterwards, replace instead of adding. 301 | */ 302 | static uint64_t store_clip(struct clip_text *ct) { 303 | static struct clip_text last_text = {NULL, CLIP_TEXT_SOURCE_MALLOC}; 304 | static time_t last_text_time; 305 | 306 | dbg("Clipboard text is considered salient, storing\n"); 307 | time_t current_time = time(NULL); 308 | uint64_t hash; 309 | 310 | if (last_text.data && 311 | difftime(current_time, last_text_time) <= PARTIAL_MAX_SECS && 312 | is_possible_partial(last_text.data, ct->data)) { 313 | dbg("Possible partial of last clip, replacing\n"); 314 | expect(cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, ct->data, &hash) == 0); 315 | } else { 316 | expect(cs_add(&cs, ct->data, &hash, 317 | cfg.deduplicate ? CS_DUPE_KEEP_LAST : CS_DUPE_KEEP_ALL) == 318 | 0); 319 | } 320 | 321 | free_clip_text(&last_text); 322 | last_text = *ct; 323 | last_text_time = current_time; 324 | 325 | // The caller no longer owns this data. 326 | ct->data = NULL; 327 | ct->source = CLIP_TEXT_SOURCE_INVALID; 328 | 329 | return hash; 330 | } 331 | 332 | /** 333 | * Process the final data collected during an INCR transfer. 334 | */ 335 | static void incr_receive_finish(struct incr_transfer *it) { 336 | enum selection_type sel = 337 | storage_atom_to_selection_type(it->property, sels); 338 | if (sel == CM_SEL_INVALID) { 339 | it_dbg(it, "Received INCR finish for unknown sel\n"); 340 | return; 341 | } 342 | 343 | it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size); 344 | char *text = malloc(it->data_size + 1); 345 | expect(text); 346 | memcpy(text, it->data, it->data_size); 347 | text[it->data_size] = '\0'; 348 | 349 | struct clip_text ct = {text, CLIP_TEXT_SOURCE_MALLOC}; 350 | 351 | char line[CS_SNIP_LINE_SIZE]; 352 | first_line(ct.data, line); 353 | it_dbg(it, "First line: %s\n", line); 354 | 355 | if (is_salient_text(ct.data)) { 356 | uint64_t hash = store_clip(&ct); 357 | maybe_trim(); 358 | if (cfg.owned_selections[sel].active && cfg.own_clipboard) { 359 | run_clipserve(hash); 360 | } 361 | } else { 362 | it_dbg(it, "Clipboard text is whitespace only, ignoring\n"); 363 | free_clip_text(&ct); 364 | } 365 | 366 | free(it->data); 367 | it_remove(&it_list, it); 368 | free(it); 369 | } 370 | 371 | #define INCR_DATA_START_BYTES 1024 * 1024 372 | 373 | /** 374 | * Acknowledge and start an INCR transfer. 375 | */ 376 | static void incr_receive_start(const XPropertyEvent *pe) { 377 | struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); 378 | expect(it); 379 | *it = (struct incr_transfer){ 380 | .property = pe->atom, 381 | .requestor = pe->window, 382 | .data_size = 0, 383 | .data_capacity = INCR_DATA_START_BYTES, 384 | .data = malloc(INCR_DATA_START_BYTES), 385 | }; 386 | expect(it->data); 387 | 388 | it_dbg(it, "Starting transfer\n"); 389 | it_add(&it_list, it); 390 | 391 | // Signal readiness for chunks 392 | XDeleteProperty(dpy, win, pe->atom); 393 | } 394 | 395 | /** 396 | * Continue receiving data during an INCR transfer. 397 | */ 398 | static void incr_receive_data(const XPropertyEvent *pe, 399 | struct incr_transfer *it) { 400 | if (pe->state != PropertyNewValue) { 401 | return; 402 | } 403 | 404 | it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size); 405 | 406 | _drop_(XFree) unsigned char *chunk = NULL; 407 | Atom actual_type; 408 | int actual_format; 409 | unsigned long nitems, bytes_after; 410 | XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType, 411 | &actual_type, &actual_format, &nitems, &bytes_after, 412 | &chunk); 413 | 414 | size_t chunk_size = nitems * (actual_format / 8); 415 | 416 | if (chunk_size == 0) { 417 | it_dbg(it, "Transfer complete\n"); 418 | incr_receive_finish(it); 419 | return; 420 | } 421 | 422 | if (it->data_size + chunk_size > it->data_capacity) { 423 | it->data_capacity = (it->data_size + chunk_size) * 2; 424 | it->data = realloc(it->data, it->data_capacity); 425 | expect(it->data); 426 | it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity); 427 | } 428 | 429 | memcpy(it->data + it->data_size, chunk, chunk_size); 430 | it->data_size += chunk_size; 431 | 432 | // Signal readiness for next chunk 433 | XDeleteProperty(dpy, win, pe->atom); 434 | } 435 | 436 | /** 437 | * Something changed in our clip storage atoms. Work out whether we want to 438 | * store the new content as a clipboard entry. 439 | */ 440 | static int handle_property_notify(const XPropertyEvent *pe) { 441 | if (pe->state != PropertyNewValue && pe->state != PropertyDelete) { 442 | return -EINVAL; 443 | } 444 | 445 | enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels); 446 | if (sel == CM_SEL_INVALID) { 447 | dbg("Received PropertyNotify for unknown sel\n"); 448 | return -EINVAL; 449 | } 450 | 451 | // Check if this property corresponds to an INCR transfer in progress 452 | struct incr_transfer *it = it_list; 453 | while (it) { 454 | if (it->property == pe->atom && it->requestor == pe->window) { 455 | break; 456 | } 457 | it = it->next; 458 | } 459 | 460 | if (it) { 461 | incr_receive_data(pe, it); 462 | return 0; 463 | } 464 | 465 | // Not an INCR transfer in progress. Check if this is an INCR transfer 466 | // starting 467 | Atom actual_type; 468 | int actual_format; 469 | unsigned long nitems, bytes_after; 470 | _drop_(XFree) unsigned char *prop = NULL; 471 | 472 | XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType, 473 | &actual_type, &actual_format, &nitems, &bytes_after, 474 | &prop); 475 | 476 | if (actual_type == incr_atom) { 477 | incr_receive_start(pe); 478 | } else { 479 | dbg("Received non-INCR PropertyNotify\n"); 480 | 481 | // store_clip will take care of freeing this later when it's gone from 482 | // last_text. 483 | struct clip_text ct = get_clipboard_text(pe->atom); 484 | if (!ct.data) { 485 | dbg("Failed to get clipboard text\n"); 486 | return -EINVAL; 487 | } 488 | char line[CS_SNIP_LINE_SIZE]; 489 | first_line(ct.data, line); 490 | dbg("First line: %s\n", line); 491 | 492 | if (is_salient_text(ct.data)) { 493 | uint64_t hash = store_clip(&ct); 494 | maybe_trim(); 495 | /* We only own CLIPBOARD because otherwise the behaviour is wonky: 496 | * 497 | * 1. When you select in a browser and press ^V, it repastes what 498 | * you have selected instead of the previous content 499 | * 2. urxvt and some other terminal emulators will unhilight on 500 | * PRIMARY ownership being taken away from them 501 | */ 502 | if (cfg.owned_selections[sel].active && cfg.own_clipboard) { 503 | run_clipserve(hash); 504 | } 505 | } else { 506 | dbg("Clipboard text is whitespace only, ignoring\n"); 507 | free_clip_text(&ct); 508 | } 509 | } 510 | 511 | return 0; 512 | } 513 | 514 | /** 515 | * Process X11 events, returning when we have either processed one clip, or 516 | * have received an indication that the selection is not owned. 517 | * 518 | * The usual sequence is: 519 | * 520 | * 1. Get an XFixesSelectionNotify that we have a new selection. 521 | * 2. Call XConvertSelection() on it to get a string in our prop. 522 | * 3. Wait for a PropertyNotify that says that's ready. 523 | * 4. When it's ready, store it, and return from the function. 524 | * 525 | * Another possible outcome, especially when trying to get the initial state at 526 | * startup, is that we get a SelectionNotify even with owner == None, which 527 | * means the selection is unowned. At that point we also return, since it's 528 | * clear that an explicit request has been nacked. 529 | */ 530 | static int handle_x11_event(int evt_base) { 531 | while (XPending(dpy)) { 532 | XEvent evt; 533 | XNextEvent(dpy, &evt); 534 | 535 | if (!enabled) { 536 | dbg("Got X event, but ignoring as collection is disabled\n"); 537 | continue; 538 | } 539 | 540 | int ret; 541 | if (evt.type == evt_base + XFixesSelectionNotify) { 542 | handle_xfixes_selection_notify((XFixesSelectionNotifyEvent *)&evt); 543 | } else if (evt.type == PropertyNotify) { 544 | ret = handle_property_notify((XPropertyEvent *)&evt); 545 | if (ret == 0) { 546 | return ret; 547 | } 548 | } else if (evt.type == SelectionNotify) { 549 | ret = handle_selection_notify((XSelectionEvent *)&evt); 550 | if (ret < 0) { 551 | return ret; 552 | } 553 | } 554 | } 555 | 556 | return -EINPROGRESS; 557 | } 558 | 559 | /** 560 | * Continuously wait for and process X11 or signal events until we fully 561 | * process success or failure for a clip. 562 | * 563 | * The usual sequence is: 564 | * 565 | * 1. Get an XFixesSelectionNotify that we have a new selection. 566 | * 2. Call XConvertSelection() on it to get a string in our prop. 567 | * 3. Wait for a PropertyNotify that says that's ready. 568 | * 4. When it's ready, store it, and return from the function. 569 | * 570 | * Another possible outcome, especially when trying to get the initial state at 571 | * startup, is that we get a SelectionNotify even with owner == None, which 572 | * means the selection is unowned. At that point we also return, since it's 573 | * clear that an explicit request has been nacked. 574 | */ 575 | static int get_one_clip(int evt_base) { 576 | while (1) { 577 | // It's possible that we have more X events to process, but because of 578 | // the way the protocol works, we won't get told about them until we 579 | // next get an event if we wait for select(). Check for them first. 580 | if (XPending(dpy)) { 581 | return handle_x11_event(evt_base); 582 | } 583 | 584 | fd_set fds; 585 | int x_fd = ConnectionNumber(dpy); 586 | 587 | FD_ZERO(&fds); 588 | FD_SET(sig_fd, &fds); 589 | FD_SET(x_fd, &fds); 590 | 591 | int max_fd = sig_fd > x_fd ? sig_fd : x_fd; 592 | expect(select(max_fd + 1, &fds, NULL, NULL, NULL) > 0); 593 | 594 | if (FD_ISSET(sig_fd, &fds)) { 595 | handle_signalfd_event(); 596 | } 597 | 598 | if (FD_ISSET(x_fd, &fds)) { 599 | return handle_x11_event(evt_base); 600 | } 601 | } 602 | } 603 | 604 | static int setup_watches(int evt_base) { 605 | XSelectInput(dpy, win, PropertyChangeMask); 606 | 607 | for (size_t i = 0; i < CM_SEL_MAX; i++) { 608 | struct selection sel = cfg.selections[i]; 609 | if (!sel.active) { 610 | continue; 611 | } 612 | Atom sel_atom = sels[i].selection; 613 | XFixesSelectSelectionInput(dpy, win, sel_atom, 614 | XFixesSetSelectionOwnerNotifyMask); 615 | dbg("Getting initial value for selection %s\n", sel.name); 616 | XConvertSelection(dpy, sel_atom, XInternAtom(dpy, "UTF8_STRING", False), 617 | sels[i].storage, win, CurrentTime); 618 | get_one_clip(evt_base); 619 | } 620 | 621 | return 0; 622 | } 623 | 624 | static int _noreturn_ run(int evt_base) { 625 | while (1) { 626 | get_one_clip(evt_base); 627 | } 628 | } 629 | 630 | #ifndef UNIT_TEST 631 | int main(int argc, char *argv[]) { 632 | (void)argv; 633 | die_on(argc != 1, "clipmenud doesn't accept any arguments\n"); 634 | int evt_base; 635 | 636 | cfg = setup("clipmenud"); 637 | exec_man_on_help(argc, argv); 638 | 639 | _drop_(close) int session_fd = 640 | open(get_session_lock_path(&cfg), O_WRONLY | O_CREAT | O_CLOEXEC, 0600); 641 | die_on(session_fd < 0, "Failed to open session file: %s\n", 642 | strerror(errno)); 643 | die_on(flock(session_fd, LOCK_EX | LOCK_NB) < 0, 644 | "Failed to lock session file -- is another clipmenud running?\n"); 645 | 646 | write_status(); 647 | 648 | _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); 649 | _drop_(close) int snip_fd = 650 | open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); 651 | expect(content_dir_fd >= 0 && snip_fd >= 0); 652 | 653 | expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); 654 | 655 | die_on(!(dpy = XOpenDisplay(NULL)), "Cannot open display\n"); 656 | win = DefaultRootWindow(dpy); 657 | setup_selections(dpy, sels); 658 | 659 | incr_atom = XInternAtom(dpy, "INCR", False); 660 | timestamp_atom = XInternAtom(dpy, "CLIPMENUD_TIMESTAMP", False); 661 | 662 | sigset_t mask; 663 | sigemptyset(&mask); 664 | sigaddset(&mask, SIGUSR1); 665 | sigaddset(&mask, SIGUSR2); 666 | sigprocmask(SIG_BLOCK, &mask, NULL); 667 | sig_fd = signalfd(-1, &mask, 0); 668 | expect(sig_fd >= 0); 669 | expect(signal(SIGCHLD, SIG_IGN) != SIG_ERR); 670 | 671 | int unused; 672 | die_on(!XFixesQueryExtension(dpy, &evt_base, &unused), "XFixes missing\n"); 673 | 674 | setup_watches(evt_base); 675 | 676 | if (!cfg.oneshot) { 677 | run(evt_base); 678 | } 679 | 680 | expect(cs_destroy(&cs) == 0); 681 | config_free(&cfg); 682 | XCloseDisplay(dpy); 683 | return 0; 684 | } 685 | #endif 686 | -------------------------------------------------------------------------------- /src/clipserve.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "config.h" 11 | #include "store.h" 12 | #include "util.h" 13 | #include "x.h" 14 | 15 | static struct incr_transfer *it_list = NULL; 16 | static Display *dpy; 17 | static Atom incr_atom; 18 | 19 | static size_t chunk_size; 20 | 21 | /** 22 | * Start an INCR transfer. 23 | */ 24 | static void incr_send_start(XSelectionRequestEvent *req, 25 | struct cs_content *content) { 26 | long incr_size = content->size; 27 | XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32, 28 | PropModeReplace, (unsigned char *)&incr_size, 1); 29 | 30 | struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); 31 | expect(it); 32 | *it = (struct incr_transfer){ 33 | .requestor = req->requestor, 34 | .property = req->property, 35 | .target = req->target, 36 | .format = 8, 37 | .data = (char *)content->data, 38 | .data_size = content->size, 39 | .offset = 0, 40 | }; 41 | 42 | it_dbg(it, "Starting transfer\n"); 43 | it_add(&it_list, it); 44 | 45 | // Listen for PropertyNotify events on the requestor window 46 | XSelectInput(dpy, it->requestor, PropertyChangeMask); 47 | } 48 | 49 | /** 50 | * Finish an INCR transfer. 51 | */ 52 | static void incr_send_finish(struct incr_transfer *it) { 53 | XChangeProperty(dpy, it->requestor, it->property, it->target, it->format, 54 | PropModeReplace, NULL, 0); 55 | it_dbg(it, "Transfer complete\n"); 56 | it_remove(&it_list, it); 57 | free(it); 58 | } 59 | 60 | /** 61 | * Continue sending data during an INCR transfer. 62 | */ 63 | static void incr_send_chunk(const XPropertyEvent *pe) { 64 | if (pe->state != PropertyDelete) { 65 | return; 66 | } 67 | 68 | struct incr_transfer *it = it_list; 69 | while (it) { 70 | if (it->requestor == pe->window && it->property == pe->atom) { 71 | size_t remaining = it->data_size - it->offset; 72 | size_t this_chunk_size = 73 | (remaining > chunk_size) ? chunk_size : remaining; 74 | 75 | it_dbg(it, 76 | "Sending chunk (bytes sent: %zu, bytes remaining: %zu)\n", 77 | it->offset, remaining); 78 | 79 | if (this_chunk_size > 0) { 80 | XChangeProperty(dpy, it->requestor, it->property, it->target, 81 | it->format, PropModeReplace, 82 | (unsigned char *)(it->data + it->offset), 83 | this_chunk_size); 84 | it->offset += this_chunk_size; 85 | } else { 86 | incr_send_finish(it); 87 | } 88 | break; 89 | } 90 | it = it->next; 91 | } 92 | } 93 | 94 | /** 95 | * Serve clipboard content for all X11 selection requests until all selections 96 | * have been claimed by another application. 97 | */ 98 | static void _nonnull_ serve_clipboard(uint64_t hash, 99 | struct cs_content *content) { 100 | bool running = true; 101 | XEvent evt; 102 | Atom targets, utf8_string, selections[2] = {XA_PRIMARY}; 103 | Window win; 104 | int remaining_selections; 105 | 106 | dpy = XOpenDisplay(NULL); 107 | expect(dpy); 108 | 109 | chunk_size = get_chunk_size(dpy); 110 | 111 | win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 0, 0, 0); 112 | XStoreName(dpy, win, "clipserve"); 113 | targets = XInternAtom(dpy, "TARGETS", False); 114 | utf8_string = XInternAtom(dpy, "UTF8_STRING", False); 115 | incr_atom = XInternAtom(dpy, "INCR", False); 116 | 117 | selections[1] = XInternAtom(dpy, "CLIPBOARD", False); 118 | for (size_t i = 0; i < arrlen(selections); i++) { 119 | bool success = false; 120 | for (int attempts = 0; attempts < 5; attempts++) { 121 | XSetSelectionOwner(dpy, selections[i], win, CurrentTime); 122 | 123 | // According to ICCCM 2.1, a client acquiring a selection should 124 | // confirm success by verifying with GetSelectionOwner. 125 | if (XGetSelectionOwner(dpy, selections[i]) == win) { 126 | success = true; 127 | break; 128 | } 129 | } 130 | if (!success) { 131 | die("Failed to set selection for %s\n", 132 | XGetAtomName(dpy, selections[i])); 133 | } 134 | } 135 | remaining_selections = arrlen(selections); 136 | 137 | while (running) { 138 | XNextEvent(dpy, &evt); 139 | switch (evt.type) { 140 | case SelectionRequest: { 141 | XSelectionRequestEvent *req = &evt.xselectionrequest; 142 | XSelectionEvent sev = {.type = SelectionNotify, 143 | .display = req->display, 144 | .requestor = req->requestor, 145 | .selection = req->selection, 146 | .time = req->time, 147 | .target = req->target, 148 | .property = req->property}; 149 | 150 | _drop_(XFree) char *window_title = 151 | get_window_title(dpy, req->requestor); 152 | dbg("Servicing request to window '%s' (0x%lX) for clip " PRI_HASH 153 | "\n", 154 | strnull(window_title), (unsigned long)req->requestor, hash); 155 | 156 | if (req->target == targets) { 157 | Atom available_targets[] = {utf8_string, XA_STRING}; 158 | XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, 159 | 32, PropModeReplace, 160 | (unsigned char *)&available_targets, 161 | arrlen(available_targets)); 162 | } else if (req->target == utf8_string || 163 | req->target == XA_STRING) { 164 | if (content->size < (off_t)chunk_size) { 165 | // Data size is small enough, send directly 166 | XChangeProperty(dpy, req->requestor, req->property, 167 | req->target, 8, PropModeReplace, 168 | (unsigned char *)content->data, 169 | (int)content->size); 170 | } else { 171 | // Initiate INCR transfer 172 | incr_send_start(req, content); 173 | } 174 | } else { 175 | sev.property = None; 176 | } 177 | 178 | XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev); 179 | break; 180 | } 181 | case SelectionClear: { 182 | if (--remaining_selections == 0) { 183 | dbg("Finished serving clip " PRI_HASH "\n", hash); 184 | running = false; 185 | } else { 186 | dbg("%d selections remaining to serve for clip " PRI_HASH 187 | "\n", 188 | remaining_selections, hash); 189 | } 190 | break; 191 | } 192 | case PropertyNotify: { 193 | incr_send_chunk(&evt.xproperty); 194 | break; 195 | } 196 | } 197 | } 198 | 199 | XCloseDisplay(dpy); 200 | } 201 | 202 | int main(int argc, char *argv[]) { 203 | die_on(argc != 2, "Usage: clipserve [hash]\n"); 204 | _drop_(config_free) struct config cfg = setup("clipserve"); 205 | exec_man_on_help(argc, argv); 206 | 207 | uint64_t hash; 208 | expect(str_to_hex64(argv[1], &hash) == 0); 209 | 210 | _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); 211 | _drop_(close) int snip_fd = 212 | open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); 213 | expect(content_dir_fd >= 0 && snip_fd >= 0); 214 | 215 | _drop_(cs_destroy) struct clip_store cs; 216 | expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); 217 | 218 | _drop_(cs_content_unmap) struct cs_content content; 219 | die_on(cs_content_get(&cs, hash, &content) < 0, 220 | "Hash " PRI_HASH " inaccessible\n", hash); 221 | 222 | serve_clipboard(hash, &content); 223 | 224 | return 0; 225 | } 226 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.h" 10 | #include "x.h" 11 | 12 | #define CLIPMENU_VERSION 7 13 | 14 | /** 15 | * Determines the runtime directory for storing application data. This is _not_ 16 | * the clip store, but the place to create the directory that will become it 17 | * (unless CM_DIR is set, in which case we use that directly). 18 | */ 19 | static const char *get_runtime_directory(void) { 20 | static const char *runtime_dir = NULL; 21 | const char *env_vars[] = {"XDG_RUNTIME_DIR", "TMPDIR"}; 22 | 23 | if (runtime_dir) { 24 | return runtime_dir; 25 | } 26 | 27 | for (size_t i = 0; i < arrlen(env_vars); i++) { 28 | const char *dir = getenv(env_vars[i]); 29 | if (dir) { 30 | return (runtime_dir = dir); 31 | } 32 | } 33 | 34 | return (runtime_dir = "/tmp"); 35 | } 36 | 37 | /** 38 | * Constructs the path to the clip store directory for clipmenu, creating the 39 | * directory if it does not exist. 40 | */ 41 | char *get_cache_dir(struct config *cfg) { 42 | expect(cfg->ready); 43 | static char cache_dir[PATH_MAX]; 44 | // In case config changed, do the write anyway 45 | snprintf_safe(cache_dir, PATH_MAX, "%s/clipmenu.%d.%ld", cfg->runtime_dir, 46 | CLIPMENU_VERSION, (long)getuid()); 47 | die_on(mkdir(cache_dir, S_IRWXU) != 0 && errno != EEXIST, 48 | "Failed to create directory: %s\n", cache_dir); 49 | return cache_dir; 50 | } 51 | 52 | /** 53 | * This whole section consists of conversion functions to go from a string in 54 | * the config file to the type we expect for `struct Config`. 55 | */ 56 | 57 | int convert_bool(const char *str, void *output) { 58 | const char *const truthy[] = {"1", "y", "yes", "true", "on"}; 59 | const char *const falsy[] = {"0", "n", "no", "false", "off"}; 60 | 61 | for (size_t i = 0; i < arrlen(truthy); i++) { 62 | if (strceq(str, truthy[i])) { 63 | *(bool *)output = true; 64 | return 0; 65 | } 66 | } 67 | 68 | for (size_t i = 0; i < arrlen(falsy); i++) { 69 | if (strceq(str, falsy[i])) { 70 | *(bool *)output = false; 71 | return 0; 72 | } 73 | } 74 | 75 | return -EINVAL; 76 | } 77 | 78 | int convert_positive_int(const char *str, void *output) { 79 | char *end; 80 | long val = strtol(str, &end, 10); 81 | if (*end != '\0' || end == str || val < 0 || val > INT_MAX) { 82 | return -EINVAL; 83 | } 84 | *(int *)output = (int)val; 85 | return 0; 86 | } 87 | 88 | int convert_ignore_window(const char *str, void *output) { 89 | struct ignore_window *iw = output; 90 | iw->set = (bool)str; 91 | if (!iw->set) { 92 | return 0; 93 | } 94 | if (regcomp(&iw->rgx, str, REG_EXTENDED | REG_NOSUB)) { 95 | return -EINVAL; 96 | } 97 | return 0; 98 | } 99 | 100 | static int convert_cm_dir(const char *str, void *output) { 101 | if (!str) { 102 | str = get_runtime_directory(); 103 | } 104 | char *rtd = strdup(str); 105 | expect(rtd); 106 | *(char **)output = rtd; 107 | return 0; 108 | } 109 | 110 | static int _nonnull_ convert_launcher(const char *str, void *output) { 111 | struct launcher *lnch = output; 112 | 113 | lnch->custom = strdup(str); 114 | expect(lnch->custom); 115 | 116 | if (streq(str, "rofi")) { 117 | lnch->ltype = LAUNCHER_ROFI; 118 | } else { 119 | lnch->ltype = LAUNCHER_CUSTOM; 120 | } 121 | 122 | return 0; 123 | } 124 | 125 | #define DEFAULT_SELECTION_STATE(name) \ 126 | (struct selection) { name, 0, NULL } 127 | 128 | static int convert_selections(const char *str, void *output) { 129 | struct selection *sels = malloc(3 * sizeof(struct selection)); 130 | expect(sels); 131 | sels[CM_SEL_CLIPBOARD] = DEFAULT_SELECTION_STATE("clipboard"); 132 | sels[CM_SEL_PRIMARY] = DEFAULT_SELECTION_STATE("primary"); 133 | sels[CM_SEL_SECONDARY] = DEFAULT_SELECTION_STATE("secondary"); 134 | 135 | _drop_(free) char *inner_str = strdup(str); 136 | expect(inner_str); 137 | const char *token = strtok(inner_str, " "); 138 | size_t i; 139 | 140 | while (token) { 141 | bool found = false; 142 | for (i = 0; i < CM_SEL_MAX; i++) { 143 | if (streq(token, sels[i].name)) { 144 | sels[i].active = true; 145 | found = true; 146 | break; 147 | } 148 | } 149 | if (!found) { 150 | return -EINVAL; 151 | } 152 | token = strtok(NULL, " "); 153 | } 154 | 155 | *(struct selection **)output = sels; 156 | 157 | return 0; 158 | } 159 | 160 | /** 161 | * Constructs the path to the clipmenu configuration file. The user can 162 | * manually specify a path with $CM_CONFIG, otherwise, it's inferred based on 163 | * $XDG_CONFIG_HOME or ~/.config. It's typically 164 | * ~/.config/clipmenu/clipmenu.conf. 165 | */ 166 | 167 | static void get_config_file(char *config_path) { 168 | const char *cm_config = getenv("CM_CONFIG"); 169 | const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); 170 | const char *home = getenv("HOME"); 171 | 172 | if (cm_config) { 173 | snprintf_safe(config_path, PATH_MAX, "%s", cm_config); 174 | } else if (xdg_config_home) { 175 | snprintf_safe(config_path, PATH_MAX, "%s/clipmenu/clipmenu.conf", 176 | xdg_config_home); 177 | } else { 178 | die_on(!home, 179 | "None of $CM_CONFIG, $XDG_CONFIG_HOME, or $HOME is set\n"); 180 | snprintf_safe(config_path, PATH_MAX, 181 | "%s/.config/clipmenu/clipmenu.conf", home); 182 | } 183 | } 184 | 185 | static int config_parse_env_vars(struct config_entry entries[], 186 | size_t entries_len) { 187 | for (size_t i = 0; i < entries_len; ++i) { 188 | const char *env_var = entries[i].env_var; 189 | if (!env_var) { 190 | continue; 191 | } 192 | const char *env_value = getenv(env_var); 193 | if (env_value && entries[i].convert(env_value, entries[i].value) != 0) { 194 | fprintf(stderr, "Error parsing environment variable for $%s\n", 195 | env_var); 196 | return -EINVAL; 197 | } else if (env_value) { 198 | dbg("Config entry %s is set to %s by $%s\n", entries[i].config_key, 199 | env_value, env_var); 200 | entries[i].is_set = true; 201 | } 202 | } 203 | 204 | return 0; 205 | } 206 | 207 | static int config_parse_file(FILE *file, struct config_entry entries[], 208 | size_t entries_len) { 209 | if (!file) { 210 | return 0; 211 | } 212 | 213 | char line[256]; 214 | while (fgets(line, sizeof(line), file)) { 215 | const char *key = strtok(line, " "); 216 | char *value = strtok(NULL, "\n"); 217 | if (!key || !value) { 218 | continue; 219 | } 220 | while (*value == ' ' || *value == '\t') { 221 | value++; 222 | } 223 | 224 | for (size_t i = 0; i < entries_len; ++i) { 225 | if (!entries[i].is_set && streq(entries[i].config_key, key)) { 226 | if (entries[i].convert(value, entries[i].value) != 0) { 227 | fprintf(stderr, "Error parsing config file for %s\n", 228 | entries[i].config_key); 229 | return -EINVAL; 230 | } 231 | dbg("Config entry %s is set to %s by config file\n", 232 | entries[i].config_key, value); 233 | entries[i].is_set = true; 234 | break; 235 | } 236 | } 237 | } 238 | 239 | return 0; 240 | } 241 | 242 | static int config_apply_default_values(struct config_entry entries[], 243 | size_t entries_len) { 244 | for (size_t i = 0; i < entries_len; ++i) { 245 | if (!entries[i].is_set) { 246 | if (entries[i].convert(entries[i].default_value, 247 | entries[i].value) != 0) { 248 | fprintf(stderr, "Error setting default value for %s\n", 249 | entries[i].config_key); 250 | return -EINVAL; 251 | } 252 | dbg("Config entry %s is set to %s by fallback\n", 253 | entries[i].config_key, entries[i].default_value); 254 | } 255 | } 256 | return 0; 257 | } 258 | 259 | /** 260 | * Parse the clipmenu configuration file and environment variables to set up 261 | * the application configuration. 262 | * 263 | * Prior to version 7, clipmenu and friends could only be configured via 264 | * environment variables, so these are supported for backwards compatibility. 265 | * In general, it's more straightforward to use the config file nowadays. 266 | * 267 | * This is generally not expected to be called by applications -- call 268 | * config_setup() instead, which provides the right file for you. 269 | */ 270 | int config_setup_internal(FILE *file, struct config *cfg) { 271 | struct config_entry entries[] = { 272 | {"max_clips", "CM_MAX_CLIPS", &cfg->max_clips, convert_positive_int, 273 | "1000", 0}, 274 | {"max_clips_batch", "CM_MAX_CLIPS_BATCH", &cfg->max_clips_batch, 275 | convert_positive_int, "100", 0}, 276 | {"oneshot", "CM_ONESHOT", &cfg->oneshot, convert_positive_int, "0", 0}, 277 | {"deduplicate", "CM_DEDUPLICATE", &cfg->deduplicate, convert_bool, "0", 278 | 0}, 279 | {"own_clipboard", "CM_OWN_CLIPBOARD", &cfg->own_clipboard, convert_bool, 280 | "0", 0}, 281 | {"selections", "CM_SELECTIONS", &cfg->selections, convert_selections, 282 | "clipboard primary", 0}, 283 | {"own_selections", "CM_OWN_SELECTIONS", &cfg->owned_selections, 284 | convert_selections, "clipboard", 0}, 285 | {"ignore_window", "CM_IGNORE_WINDOW", &cfg->ignore_window, 286 | convert_ignore_window, NULL, 0}, 287 | {"launcher", "CM_LAUNCHER", &cfg->launcher, convert_launcher, "dmenu", 288 | 0}, 289 | {"launcher_pass_dmenu_args", "CM_LAUNCHER_PASS_DMENU_ARGS", 290 | &cfg->launcher_pass_dmenu_args, convert_bool, "1", 0}, 291 | {"touch_on_select", NULL, &cfg->touch_on_select, convert_bool, "0", 0}, 292 | {"cm_dir", "CM_DIR", &cfg->runtime_dir, convert_cm_dir, NULL, 0}}; 293 | 294 | size_t entries_len = arrlen(entries); 295 | 296 | int ret = config_parse_env_vars(entries, entries_len); 297 | if (ret < 0) { 298 | return ret; 299 | } 300 | ret = config_parse_file(file, entries, entries_len); 301 | if (ret < 0) { 302 | return ret; 303 | } 304 | ret = config_apply_default_values(entries, entries_len); 305 | if (ret < 0) { 306 | return ret; 307 | } 308 | 309 | cfg->ready = true; 310 | 311 | return 0; 312 | } 313 | 314 | /** 315 | * Frees dynamically allocated memory within the config structure. 316 | */ 317 | void config_free(struct config *cfg) { 318 | free(cfg->runtime_dir); 319 | free(cfg->launcher.custom); 320 | free(cfg->selections); 321 | free(cfg->owned_selections); 322 | if (cfg->ignore_window.set) { 323 | regfree(&cfg->ignore_window.rgx); 324 | } 325 | } 326 | 327 | /** 328 | * Initialise the clipmenu configuration by loading settings from environment 329 | * variables and the configuration file. 330 | */ 331 | static void config_setup(struct config *cfg) { 332 | char config_path[PATH_MAX]; 333 | get_config_file(config_path); 334 | _drop_(fclose) FILE *file = fopen(config_path, "r"); 335 | expect(file || errno == ENOENT); 336 | die_on(config_setup_internal(file, cfg) != 0, "Invalid config\n"); 337 | } 338 | 339 | static char stdout_buf[512]; 340 | const char *prog_name = "broken"; 341 | 342 | /** 343 | * Performs initial setup for clipmenu applications, including setting the 344 | * program name (used for dbg()), making sure stdout is line buffered, getting 345 | * the config, and setting up X11 error handling. 346 | */ 347 | struct config setup(const char *inner_prog_name) { 348 | struct config cfg; 349 | prog_name = inner_prog_name; 350 | expect(setvbuf(stdout, stdout_buf, _IOLBF, sizeof(stdout_buf)) == 0); 351 | config_setup(&cfg); 352 | XSetErrorHandler(xerror_handler); 353 | return cfg; 354 | } 355 | 356 | void setup_selections(Display *dpy, struct cm_selections *sels) { 357 | sels[CM_SEL_CLIPBOARD].selection = XInternAtom(dpy, "CLIPBOARD", False); 358 | sels[CM_SEL_CLIPBOARD].storage = 359 | XInternAtom(dpy, "CLIPMENUD_CUR_CLIPBOARD", False); 360 | sels[CM_SEL_PRIMARY].selection = XA_PRIMARY; 361 | sels[CM_SEL_PRIMARY].storage = 362 | XInternAtom(dpy, "CLIPMENUD_CUR_PRIMARY", False); 363 | sels[CM_SEL_SECONDARY].selection = XA_SECONDARY; 364 | sels[CM_SEL_SECONDARY].storage = 365 | XInternAtom(dpy, "CLIPMENUD_CUR_SECONDARY", False); 366 | } 367 | 368 | /** 369 | * Maps an Atom to a selection_type based on the selection atoms in the 370 | * provided cm_selections struct. Returns CM_SEL_INVALID on error if the 371 | * selection type is not found. 372 | */ 373 | enum selection_type 374 | selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { 375 | for (size_t i = 0; i < CM_SEL_MAX; ++i) { 376 | if (sels[i].selection == atom) { 377 | return i; 378 | } 379 | } 380 | return CM_SEL_INVALID; 381 | } 382 | 383 | /** 384 | * Maps an Atom to a selection_type based on the storage atoms in the provided 385 | * cm_selections struct. Returns CM_SEL_INVALID on error if the storage type is 386 | * not found. 387 | */ 388 | enum selection_type 389 | storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { 390 | for (size_t i = 0; i < CM_SEL_MAX; ++i) { 391 | if (sels[i].storage == atom) { 392 | return i; 393 | } 394 | } 395 | return CM_SEL_INVALID; 396 | } 397 | 398 | void exec_man(void) { 399 | execlp("man", "man", prog_name, NULL); 400 | die("Failed to exec man: %s\n", strerror(errno)); 401 | } 402 | 403 | // cppcheck doesn't know this comes from an outside type 404 | // cppcheck-suppress [constParameter,unmatchedSuppression] 405 | void exec_man_on_help(int argc, char *argv[]) { 406 | for (int i = 1; i < argc; i++) { 407 | if (streq(argv[i], "--")) { 408 | break; 409 | } 410 | if (streq(argv[i], "-h") || streq(argv[i], "--help")) { 411 | exec_man(); 412 | } 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CM_CONFIG_H 2 | #define CM_CONFIG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "util.h" 11 | 12 | struct selection { 13 | const char *name; 14 | bool active; 15 | Atom *atom; 16 | }; 17 | enum selection_type { 18 | CM_SEL_CLIPBOARD, 19 | CM_SEL_PRIMARY, 20 | CM_SEL_SECONDARY, 21 | CM_SEL_MAX, 22 | CM_SEL_INVALID 23 | }; 24 | struct cm_selections { 25 | Atom selection; 26 | Atom storage; 27 | }; 28 | struct ignore_window { 29 | bool set; 30 | regex_t rgx; 31 | }; 32 | enum launcher_known { 33 | LAUNCHER_ROFI, 34 | LAUNCHER_CUSTOM, 35 | }; 36 | struct launcher { 37 | enum launcher_known ltype; 38 | char *custom; 39 | }; 40 | struct config { 41 | bool ready; 42 | bool debug; 43 | char *runtime_dir; 44 | int max_clips; 45 | int max_clips_batch; 46 | int oneshot; 47 | bool deduplicate; 48 | bool own_clipboard; 49 | struct selection *owned_selections; 50 | struct selection *selections; 51 | struct ignore_window ignore_window; 52 | struct launcher launcher; 53 | bool launcher_pass_dmenu_args; 54 | bool touch_on_select; 55 | }; 56 | typedef int (*conversion_func_t)(const char *, void *); 57 | struct config_entry { 58 | const char *config_key; 59 | const char *env_var; 60 | void *value; 61 | conversion_func_t convert; 62 | const char *default_value; 63 | bool is_set; 64 | }; 65 | 66 | char *get_cache_dir(struct config *cfg); 67 | 68 | void exec_man(void); 69 | void exec_man_on_help(int argc, char *argv[]); 70 | 71 | /** 72 | * Define a function that generates and caches a path within the application's 73 | * cache directory. 74 | * 75 | * For example, DEFINE_GET_PATH_FUNCTION(foo) defines a function `get_foo_path` 76 | * that returns the path to "foo" within the cache directory. 77 | */ 78 | #define DEFINE_GET_PATH_FUNCTION(name) \ 79 | static inline char *get_##name##_path(struct config *cfg) { \ 80 | static char path[PATH_MAX]; \ 81 | /* Just in case the config changed, do the write anyway */ \ 82 | snprintf_safe(path, PATH_MAX, "%s/" #name, get_cache_dir(cfg)); \ 83 | return path; \ 84 | } 85 | 86 | DEFINE_GET_PATH_FUNCTION(line_cache) 87 | DEFINE_GET_PATH_FUNCTION(enabled) 88 | DEFINE_GET_PATH_FUNCTION(session_lock) 89 | 90 | extern const char *prog_name; 91 | struct config _nonnull_ setup(const char *inner_prog_name); 92 | void _nonnull_ setup_selections(Display *dpy, struct cm_selections *sels); 93 | enum selection_type _nonnull_ 94 | selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels); 95 | enum selection_type _nonnull_ 96 | storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels); 97 | 98 | int convert_bool(const char *str, void *output); 99 | int convert_positive_int(const char *str, void *output); 100 | int convert_ignore_window(const char *str, void *output); 101 | int config_setup_internal(FILE *file, struct config *cfg); 102 | void config_free(struct config *cfg); 103 | DEFINE_DROP_FUNC_PTR(struct config, config_free) 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /src/store.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "store.h" 16 | 17 | /** 18 | * TERMINOLOGY 19 | * 20 | * - Clip store: A pairing of a file containing snips and a content directory 21 | * - Clip store entry: A single pair of snip and content entry 22 | * - Snip file: A file which maps lines to content entries using hashes 23 | * - Snip: A (hash, line) pair in the clip store, representing a content entry 24 | * - Content entry: The full data stored from the clipboard 25 | * - Content directory: The directory in which the content entries are stored 26 | * 27 | * KEY FUNCTIONS: 28 | * 29 | * - cs_add - add a clip store entry 30 | * - cs_remove - remove a clip store entry by callback 31 | * - cs_trim - trim to the newest/oldest N entries 32 | * - cs_snip_iter - iterate over snip hashes and lines 33 | * - cs_content_get - get the content for a snip hash 34 | * 35 | * CLIP STORE DESIGN 36 | * 37 | * Our primary focus is on achieving high efficiency in appending new snips, 38 | * iterating over the entire list of snips, and replacing the final snip in the 39 | * snip file. For this reason complexity is avoided in processing deletions, 40 | * since deletions are usually rare. This avoids us having to implement (for 41 | * example) tombstones, which would slow things down and complicate iteration. 42 | * 43 | * CONTENT DIRECTORY DESIGN 44 | * 45 | * The content directory is extremely simple: it contains files with the same 46 | * name as the snip hash. This allows quickly going from the one line summary 47 | * in the snip to the full contents. 48 | * 49 | * SYNCHRONISATION 50 | * 51 | * The clip store's size may be increased or decreased by another program using 52 | * the library, so before each operation we must take a lock and check if the 53 | * header was updated. If it was, we update the size of the mmapped area to 54 | * suit. The lock is implemented using flock() on cs->snip_fd, see cs_ref(), 55 | * cs_ref_no_update(), and cs_unref(). 56 | */ 57 | 58 | /** 59 | * Calculate the needed file size in bytes for @nr_snips snips, adding the 60 | * header. 61 | * 62 | * @nr_snips: The number of snips to calculate for 63 | */ 64 | static size_t _must_use_ cs_file_size(size_t nr_snips) { 65 | return (nr_snips + 1) * CS_SNIP_SIZE; 66 | } 67 | 68 | /** 69 | * Validate the consistency of the clip store's header information. 70 | * 71 | * @cs: The clip store to operate on 72 | * @file_size: The current size of the file 73 | */ 74 | static int _must_use_ _nonnull_ cs_header_validate(const struct clip_store *cs, 75 | size_t file_size) { 76 | if (cs->header->nr_snips > cs->header->nr_snips_alloc || 77 | (cs->header->nr_snips_alloc + 1) * CS_SNIP_SIZE != file_size) { 78 | return -EINVAL; 79 | } 80 | return 0; 81 | } 82 | 83 | /** 84 | * Decrease the reference count for the clip store lock, unrefing it if 85 | * the refcount reaches zero. 86 | * 87 | * @cs: The clip store to operate on 88 | */ 89 | void cs_unref(struct clip_store *cs) { 90 | expect(cs->refcount > 0); 91 | cs->refcount--; 92 | if (cs->refcount == 0) { 93 | expect(flock(cs->snip_fd, LOCK_UN) == 0); 94 | } 95 | } 96 | 97 | /** 98 | * Increase the reference count for the clip store lock. 99 | * 100 | * @cs: The clip store to operate on 101 | */ 102 | static struct ref_guard _must_use_ _nonnull_ 103 | cs_ref_no_update(struct clip_store *cs) { 104 | struct ref_guard guard = {.status = 0, .unref = cs_unref, .cs = cs}; 105 | if (cs->refcount == 0) { 106 | expect(flock(cs->snip_fd, LOCK_EX) == 0); 107 | } 108 | static_assert(sizeof(cs->refcount) == sizeof(size_t), 109 | "refcount type wrong"); 110 | expect(cs->refcount < SIZE_MAX); 111 | cs->refcount++; 112 | return guard; 113 | } 114 | 115 | /** 116 | * Increase the reference count for the clip store lock, and remap as needed if 117 | * the header values have changed. 118 | * 119 | * Even if the guard status indicates an error, you must still call cs_unref(). 120 | * 121 | * @cs: The clip store to operate on 122 | */ 123 | struct ref_guard cs_ref(struct clip_store *cs) { 124 | struct ref_guard guard = cs_ref_no_update(cs); 125 | 126 | if (cs->refcount > 1) { 127 | // We're an inner reference, so any necessary remapping has already 128 | // been performed. 129 | return guard; 130 | } 131 | 132 | if (cs->local_nr_snips != cs->header->nr_snips || 133 | cs->local_nr_snips_alloc != cs->header->nr_snips_alloc) { 134 | struct stat st; 135 | if (fstat(cs->snip_fd, &st) < 0) { 136 | guard.status = negative_errno(); 137 | return guard; 138 | } 139 | 140 | int ret = cs_header_validate(cs, (size_t)st.st_size); 141 | if (ret < 0) { 142 | guard.status = ret; 143 | return guard; 144 | } 145 | 146 | // If we shrank, no need to remap, since we'll just use the new bounds. 147 | if (cs->local_nr_snips_alloc < cs->header->nr_snips_alloc) { 148 | struct cs_header *new_header = mremap( 149 | cs->header, cs_file_size(cs->local_nr_snips_alloc), 150 | cs_file_size(cs->header->nr_snips_alloc), MREMAP_MAYMOVE); 151 | if (new_header == MAP_FAILED) { 152 | guard.status = negative_errno(); 153 | return guard; 154 | } 155 | 156 | cs->header = new_header; 157 | cs->snips = (struct cs_snip *)(cs->header + 1); 158 | } 159 | 160 | cs->local_nr_snips = cs->header->nr_snips; 161 | cs->local_nr_snips_alloc = cs->header->nr_snips_alloc; 162 | } 163 | 164 | return guard; 165 | } 166 | 167 | /** 168 | * _drop_() function for when a `ref_guard` structure goes out of scope. 169 | * 170 | * @guard: The guard lock 171 | */ 172 | void drop_cs_unref(struct ref_guard *guard) { 173 | guard->status = -EBADF; 174 | guard->unref(guard->cs); 175 | } 176 | 177 | /** 178 | * Destroy the clip store, releasing all of its resources. 179 | * 180 | * @cs: The clip store to operate on 181 | */ 182 | int cs_destroy(struct clip_store *cs) { 183 | cs->ready = false; 184 | // Don't use the value from the header: if it's out of date, we haven't 185 | // done mremap() with the new size yet 186 | if (munmap(cs->header, cs_file_size(cs->local_nr_snips_alloc))) { 187 | return negative_errno(); 188 | } 189 | return 0; 190 | } 191 | 192 | /** 193 | * _drop_() function for when a `clip_store` goes out of scope. 194 | * 195 | * @guard: The guard lock 196 | */ 197 | void drop_cs_destroy(struct clip_store *cs) { expect(cs_destroy(cs) == 0); } 198 | 199 | /** 200 | * Initialise a `struct clip_store` with snip_fd open to a file for snip 201 | * storage and content_fd open to a directory for content entry storage. 202 | * 203 | * The snip file is extended and the header snip is written if the file size is 204 | * zero. The file is mapped into memory until cs_destroy() is called. 205 | * 206 | * @cs: The clip store to initialise 207 | * @snip_fd: Open file descriptor for the snip file 208 | * @content_dir_fd: Open file descriptor for the content directory 209 | */ 210 | int cs_init(struct clip_store *cs, int snip_fd, int content_dir_fd) { 211 | cs->ready = false; 212 | cs->snip_fd = snip_fd; 213 | cs->content_dir_fd = content_dir_fd; 214 | cs->refcount = 0; 215 | _drop_(cs_unref) struct ref_guard guard = cs_ref_no_update(cs); 216 | 217 | struct stat st; 218 | if (fstat(snip_fd, &st) < 0) { 219 | return negative_errno(); 220 | } 221 | if (st.st_size % CS_SNIP_SIZE != 0) { 222 | return -EINVAL; 223 | } 224 | 225 | size_t file_size = (size_t)st.st_size; 226 | if (file_size == 0) { 227 | file_size = CS_SNIP_SIZE; 228 | if (ftruncate(snip_fd, (off_t)file_size) < 0) { 229 | return negative_errno(); 230 | } 231 | } 232 | 233 | cs->header = 234 | mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, snip_fd, 0); 235 | if (cs->header == MAP_FAILED) { 236 | return negative_errno(); 237 | } 238 | 239 | int ret = cs_header_validate(cs, file_size); 240 | if (ret < 0) { 241 | munmap(cs->header, file_size); 242 | return ret; 243 | } 244 | 245 | cs->snips = (struct cs_snip *)(cs->header + 1); 246 | cs->local_nr_snips = cs->header->nr_snips; 247 | cs->local_nr_snips_alloc = cs->header->nr_snips_alloc; 248 | cs->ready = true; 249 | 250 | (void)guard; // Old clang will complain guard is unused, despite cleanup 251 | 252 | return 0; 253 | } 254 | 255 | /** 256 | * Round up a number to the nearest multiple of a specified step. 257 | * 258 | * @n: The number to round up 259 | * @step: The step size to round up to 260 | */ 261 | static size_t round_up(size_t n, size_t step) { 262 | return ((n + step - 1) / step) * step; 263 | } 264 | 265 | /** 266 | * Adjust the size of the mmapped file used by the clip store to the specified 267 | * new size. It first attempts to resize the file using ftruncate(). If this 268 | * operation is successful, it then remaps the memory mapping to reflect the 269 | * new size of the file. 270 | * 271 | * WARNING: cs->snips and cs->header may move after cs_file_resize(), so 272 | * copied pointers from before invocation must not be used after calling this. 273 | * 274 | * @cs: The clip store to operate on 275 | * @new_nr_snips: The new number of snips in the snip file 276 | */ 277 | static int _must_use_ _nonnull_ cs_file_resize(struct clip_store *cs, 278 | size_t new_nr_snips) { 279 | bool grow = new_nr_snips >= cs->header->nr_snips; 280 | 281 | if (grow && new_nr_snips <= cs->header->nr_snips_alloc) { 282 | cs->header->nr_snips = new_nr_snips; 283 | return 0; 284 | } 285 | 286 | /* If this is a shrink, do it exactly: someone may be deleting sensitive 287 | * snips and so it's best to remove them immediately. Otherwise, batch 288 | * allocate. */ 289 | size_t new_nr_snips_alloc = 290 | grow ? round_up(new_nr_snips, CS_SNIP_ALLOC_BATCH) : new_nr_snips; 291 | 292 | size_t new_size = cs_file_size(new_nr_snips_alloc); 293 | if (ftruncate(cs->snip_fd, (off_t)new_size) < 0) { 294 | return negative_errno(); 295 | } 296 | 297 | if (grow) { 298 | struct cs_header *new_snips = 299 | mremap(cs->header, cs_file_size(cs->header->nr_snips_alloc), 300 | new_size, MREMAP_MAYMOVE); 301 | if (new_snips == MAP_FAILED) { 302 | return negative_errno(); 303 | } 304 | cs->header = new_snips; 305 | cs->snips = (struct cs_snip *)cs->header + 1; 306 | } 307 | 308 | cs->header->nr_snips = cs->local_nr_snips = new_nr_snips; 309 | cs->header->nr_snips_alloc = cs->local_nr_snips_alloc = new_nr_snips_alloc; 310 | 311 | return 0; 312 | } 313 | 314 | /** 315 | * Update a clip store snip to contain the specified hash and line content. 316 | * 317 | * @snip: Pointer to the snip to modify 318 | * @hash: The new hash value for the snip 319 | * @line: The new line content for the snip 320 | * @nr_lines: The number of lines in the line content 321 | */ 322 | static void _nonnull_ cs_snip_update(struct cs_snip *snip, uint64_t hash, 323 | const char *line, uint64_t nr_lines) { 324 | snip->hash = hash; 325 | snip->doomed = false; 326 | snip->nr_lines = nr_lines; 327 | strncpy(snip->line, line, CS_SNIP_LINE_SIZE - 1); 328 | snip->line[CS_SNIP_LINE_SIZE - 1] = '\0'; 329 | } 330 | 331 | /** 332 | * Computes a 64-bit FNV-1a hash for a given buffer. 333 | * 334 | * @buf: The input buffer to hash. 335 | */ 336 | static uint64_t fnv1a_64_hash(const char *buf) { 337 | const uint64_t fnv_offset_basis = 0xcbf29ce484222325ULL; 338 | const uint64_t fnv_prime = 0x100000001b3ULL; 339 | uint64_t hash = fnv_offset_basis; 340 | const uint8_t *src = (const uint8_t *)buf; 341 | while (*src) { 342 | hash ^= *src++; 343 | hash *= fnv_prime; 344 | } 345 | return hash; 346 | } 347 | 348 | /** 349 | * Extracts the first non-empty line from a given text buffer and copies it to 350 | * the output buffer. Returns the total number of lines. A final line with no 351 | * newline is considered a line for accounting purposes. 352 | * 353 | * @text: The input text buffer 354 | * @out: The output buffer. Must be at least CS_SNIP_LINE_SIZE bytes 355 | */ 356 | size_t first_line(const char *text, char *out) { 357 | bool found = false; 358 | size_t nr_lines = 0; 359 | const char *cur = text; 360 | 361 | out[0] = '\0'; 362 | 363 | for (; *cur; cur++) { 364 | nr_lines += (*cur == '\n'); 365 | if (!found && *cur != '\n') { 366 | found = true; 367 | snprintf(out, CS_SNIP_LINE_SIZE, "%.*s", (int)strcspn(cur, "\n"), 368 | cur); 369 | } 370 | } 371 | 372 | return nr_lines + (found && *--cur != '\n'); 373 | } 374 | 375 | /** 376 | * Add a new snip consisting of a hash value and a line of text to the end of 377 | * the clip store. The snip file size is grown as necessary to accommodate the 378 | * new snip. 379 | * 380 | * @cs: The clip store to operate on 381 | * @hash: The hash value of the snip to add 382 | * @line: The line content of the snip to add 383 | * @nr_lines: The number of lines in the line content 384 | */ 385 | static int _must_use_ _nonnull_ cs_snip_add(struct clip_store *cs, 386 | uint64_t hash, const char *line, 387 | uint64_t nr_lines) { 388 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 389 | if (guard.status < 0) { 390 | return guard.status; 391 | } 392 | int ret = cs_file_resize(cs, cs->header->nr_snips + 1); 393 | if (ret < 0) { 394 | return ret; 395 | } 396 | cs_snip_update(cs->snips + cs->header->nr_snips - 1, hash, line, nr_lines); 397 | return 0; 398 | } 399 | 400 | /** 401 | * Add content to the content directory using the hash as the filename. 402 | * 403 | * @cs: The clip store to operate on 404 | * @hash: The hash of the content to add 405 | * @content: The content to add to the file 406 | * @dupe_policy: If set to CS_DUPE_KEEP_LAST, will return with -EEXIST when 407 | * trying to insert duplicate entry. 408 | */ 409 | static int _must_use_ _nonnull_ 410 | cs_content_add(struct clip_store *cs, uint64_t hash, const char *content, 411 | enum cs_dupe_policy dupe_policy) { 412 | bool dupe = false; 413 | 414 | char dir_path[CS_HASH_STR_MAX]; 415 | snprintf(dir_path, sizeof(dir_path), PRI_HASH, hash); 416 | 417 | int ret = mkdirat(cs->content_dir_fd, dir_path, 0700); 418 | if (ret < 0) { 419 | if (errno != EEXIST || dupe_policy == CS_DUPE_KEEP_LAST) { 420 | return negative_errno(); 421 | } 422 | dupe = true; 423 | } 424 | 425 | char base_file_path[PATH_MAX]; 426 | snprintf(base_file_path, sizeof(base_file_path), "%s/1", dir_path); 427 | 428 | if (dupe) { 429 | // This clip already exists, just create a link for refcounting 430 | struct stat st; 431 | if (fstatat(cs->content_dir_fd, base_file_path, &st, 0) < 0) { 432 | return negative_errno(); 433 | } 434 | 435 | size_t link_num = (size_t)st.st_nlink + 1; 436 | char linkpath[PATH_MAX]; 437 | snprintf(linkpath, sizeof(linkpath), "%s/%zu", dir_path, link_num); 438 | if (linkat(cs->content_dir_fd, base_file_path, cs->content_dir_fd, 439 | linkpath, 0) < 0) { 440 | return negative_errno(); 441 | } 442 | 443 | return 0; 444 | } 445 | 446 | // This is a new clip 447 | _drop_(close) int fd = openat(cs->content_dir_fd, base_file_path, 448 | O_WRONLY | O_CREAT | O_EXCL, 0600); 449 | if (fd < 0) { 450 | return negative_errno(); 451 | } 452 | 453 | const char *cur = content; 454 | size_t remaining = strlen(content); 455 | 456 | while (remaining > 0) { 457 | ssize_t written = write(fd, cur, remaining); 458 | if (written < 0) { 459 | return negative_errno(); 460 | } 461 | remaining -= (size_t)written; 462 | cur += written; 463 | } 464 | 465 | return 0; 466 | } 467 | 468 | /** 469 | * Clean up the backing mapped data and file descriptor for a `struct 470 | * cs_content` object. 471 | * 472 | * @content: The content to unmap 473 | */ 474 | int cs_content_unmap(struct cs_content *content) { 475 | if (content && content->data) { 476 | close(content->fd); 477 | if (munmap(content->data, (size_t)content->size)) { 478 | return negative_errno(); 479 | } 480 | } 481 | return 0; 482 | } 483 | 484 | /** 485 | * _drop_ function for cs_content_unmap() 486 | * 487 | * @content: The content to unmap 488 | */ 489 | void drop_cs_content_unmap(struct cs_content *content) { 490 | int ret = cs_content_unmap(content); 491 | expect(ret == 0); 492 | } 493 | 494 | /** 495 | * Retrieve the content associated with a given hash from the content directory 496 | * and map it into memory. 497 | * 498 | * @cs: The clip store to operate on 499 | * @hash: The hash of the content to retrieve 500 | * @content: A pointer to a `struct cs_content` to populate. The caller must 501 | * call cs_content_unmap() when done to free it 502 | */ 503 | int cs_content_get(struct clip_store *cs, uint64_t hash, 504 | struct cs_content *content) { 505 | memset(content, '\0', sizeof(struct cs_content)); 506 | 507 | char filename[PATH_MAX]; 508 | snprintf(filename, sizeof(filename), PRI_HASH "/1", hash); 509 | 510 | _drop_(close) int fd = openat(cs->content_dir_fd, filename, O_RDONLY); 511 | if (fd < 0) { 512 | return negative_errno(); 513 | } 514 | 515 | struct stat st; 516 | if (fstat(fd, &st) < 0) { 517 | return negative_errno(); 518 | } 519 | 520 | char *data = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); 521 | if (data == MAP_FAILED) { 522 | return negative_errno(); 523 | } 524 | 525 | content->data = data; 526 | content->fd = fd; 527 | content->size = st.st_size; 528 | 529 | return 0; 530 | } 531 | 532 | /** 533 | * Move the entry with the specified hash to the newest slot. 534 | * 535 | * @cs: The clip store to operate on 536 | * @hash: The hash of the entry to move 537 | */ 538 | int cs_make_newest(struct clip_store *cs, uint64_t hash) { 539 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 540 | if (guard.status < 0) { 541 | return guard.status; 542 | } 543 | 544 | for (int i = 0; i < (int)cs->local_nr_snips; ++i) { 545 | if (cs->snips[i].hash == hash) { 546 | struct cs_snip tmp = cs->snips[i]; 547 | memmove(cs->snips + i, cs->snips + i + 1, 548 | (cs->local_nr_snips - (i + 1)) * sizeof(*cs->snips)); 549 | cs->snips[cs->local_nr_snips - 1] = tmp; 550 | return 0; 551 | } 552 | } 553 | die("unreachable"); 554 | } 555 | 556 | /** 557 | * Add a new content entry to the clip store and content directory. 558 | * 559 | * @cs: The clip store to operate on 560 | * @content: The content to add 561 | * @out_hash: Output for the generated hash, or NULL 562 | * @dupe_policy: Policy to use for duplicate entries 563 | */ 564 | int cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash, 565 | enum cs_dupe_policy dupe_policy) { 566 | uint64_t hash = fnv1a_64_hash(content); 567 | char line[CS_SNIP_LINE_SIZE]; 568 | size_t nr_lines = first_line(content, line); 569 | 570 | if (out_hash) { 571 | *out_hash = hash; 572 | } 573 | 574 | int ret = cs_content_add(cs, hash, content, dupe_policy); 575 | if (ret == -EEXIST && dupe_policy == CS_DUPE_KEEP_LAST) { 576 | return cs_make_newest(cs, hash); 577 | } 578 | return ret ? ret : cs_snip_add(cs, hash, line, nr_lines); 579 | } 580 | 581 | /** 582 | * Iterate over the snips in the clip store. The function should be initially 583 | * called with *snip set to NULL, which will set *snip to point to the newest 584 | * snip (excluding the header). On subsequent calls, *snip is updated to point 585 | * to the next snip in the snip file. The iteration stops when there are no 586 | * more snips to process, indicated by the function returning false. 587 | * 588 | * @guard: The guard lock 589 | * @snip: Pointer to a pointer to the current snip being iterated over 590 | * @direction: Whether to iterate from the oldest to newest or vice versa 591 | */ 592 | bool cs_snip_iter(struct ref_guard *guard, enum cs_iter_direction direction, 593 | struct cs_snip **snip) { 594 | if (guard->status < 0 || guard->cs->header->nr_snips == 0) { 595 | return false; 596 | } 597 | 598 | struct cs_snip *oldest = guard->cs->snips; 599 | struct cs_snip *newest = oldest + (guard->cs->header->nr_snips - 1); 600 | const struct cs_snip *stop = 601 | direction == CS_ITER_NEWEST_FIRST ? oldest : newest; 602 | 603 | if (!*snip) { 604 | *snip = direction == CS_ITER_NEWEST_FIRST ? newest : oldest; 605 | return true; 606 | } else if (*snip != stop) { 607 | *snip = *snip + (direction == CS_ITER_NEWEST_FIRST ? -1 : 1); 608 | return true; 609 | } 610 | return false; 611 | } 612 | 613 | /** 614 | * Remove content from the content directory using the hash as the filename. 615 | * 616 | * @cs: The clip store to operate on 617 | * @hash: The hash of the content to remove 618 | */ 619 | static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs, 620 | uint64_t hash) { 621 | 622 | char hash_dir_name[CS_HASH_STR_MAX]; 623 | snprintf(hash_dir_name, sizeof(hash_dir_name), PRI_HASH, hash); 624 | 625 | _drop_(close) int hash_dir_fd = 626 | openat(cs->content_dir_fd, hash_dir_name, O_RDONLY); 627 | if (hash_dir_fd < 0) { 628 | return negative_errno(); 629 | } 630 | 631 | struct stat st; 632 | if (fstatat(hash_dir_fd, "1", &st, 0) < 0) { 633 | return negative_errno(); 634 | } 635 | 636 | char nlink_path[PATH_MAX]; 637 | snprintf(nlink_path, sizeof(nlink_path), "%u", (unsigned)st.st_nlink); 638 | 639 | if (unlinkat(hash_dir_fd, nlink_path, 0) < 0) { 640 | return negative_errno(); 641 | } 642 | 643 | if (st.st_nlink == 1 && 644 | unlinkat(cs->content_dir_fd, hash_dir_name, AT_REMOVEDIR) < 0) { 645 | return negative_errno(); 646 | } 647 | 648 | return 0; 649 | } 650 | 651 | /** 652 | * Compacts the clip store by removing doomed snips, finalising their removal 653 | * after being marked in cs_remove(). 654 | * 655 | * @guard: The guard lock 656 | */ 657 | static size_t _nonnull_ cs_snip_remove_doomed(struct ref_guard *guard) { 658 | size_t nr_doomed = 0; 659 | struct cs_snip *snip = NULL; 660 | 661 | while (cs_snip_iter(guard, CS_ITER_OLDEST_FIRST, &snip)) { 662 | if (snip->doomed) { 663 | nr_doomed++; 664 | } else if (nr_doomed > 0) { 665 | *(snip - nr_doomed) = *snip; 666 | } 667 | } 668 | 669 | return nr_doomed; 670 | } 671 | 672 | /** 673 | * Iterate over the specified number of snips in the clip store from newest 674 | * to oldest and remove those for which the predicate function returns 675 | * CS_ACTION_REMOVE (see `enum cs_remove_action` above). 676 | * 677 | * @cs: The clip store to operate on 678 | * @should_remove: Function pointer to the predicate used to decide removal 679 | * @private: Pointer to user-defined data passed to the predicate function 680 | * @direction: Whether to iterate from the oldest to newest or vice versa 681 | */ 682 | int cs_remove(struct clip_store *cs, enum cs_iter_direction direction, 683 | enum cs_remove_action (*should_remove)(uint64_t, const char *, 684 | void *), 685 | void *private) { 686 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 687 | if (guard.status < 0) { 688 | return guard.status; 689 | } 690 | 691 | bool found = false; 692 | struct cs_snip *snip = NULL; 693 | 694 | while (cs_snip_iter(&guard, direction, &snip)) { 695 | enum cs_remove_action action = 696 | should_remove(snip->hash, snip->line, private); 697 | 698 | if (action & CS_ACTION_REMOVE) { 699 | found = true; 700 | int ret = cs_content_remove(cs, snip->hash); 701 | if (ret < 0) { 702 | return ret; 703 | } 704 | snip->doomed = true; 705 | } 706 | if (action & CS_ACTION_STOP) { 707 | break; 708 | } 709 | } 710 | 711 | if (!found) { 712 | return 0; 713 | } 714 | 715 | size_t nr_doomed = cs_snip_remove_doomed(&guard); 716 | int ret = cs_file_resize(cs, cs->header->nr_snips - nr_doomed); 717 | if (ret < 0) { 718 | return ret; 719 | } 720 | 721 | return 0; 722 | } 723 | 724 | /** 725 | * Callback function for deciding which snips to remove during trim operation. 726 | * 727 | * @hash: Unused 728 | * @line: Unused 729 | * @private: The count of remaining entries to trim 730 | */ 731 | static enum cs_remove_action _must_use_ _nonnull_ 732 | trim_callback(uint64_t hash, const char *line, void *private) { 733 | (void)hash; 734 | (void)line; 735 | 736 | size_t *count = private; 737 | if (*count == 0) { 738 | return CS_ACTION_REMOVE; 739 | } 740 | (*count)--; 741 | return CS_ACTION_KEEP; 742 | } 743 | 744 | /** 745 | * Trim the clip store to only retain the specified number of snips. 746 | * 747 | * @cs: The clip store to operate on 748 | * @direction: Whether to remove the N newest or N oldest 749 | * @nr_keep: The number of newest snips to retain 750 | */ 751 | int cs_trim(struct clip_store *cs, enum cs_iter_direction direction, 752 | size_t nr_keep) { 753 | if (nr_keep >= cs->header->nr_snips) { 754 | return 0; // No action needed if we're keeping everything or more 755 | } 756 | 757 | int ret = cs_remove(cs, direction, trim_callback, &nr_keep); 758 | if (ret < 0) { 759 | return ret; 760 | } 761 | 762 | return 0; 763 | } 764 | 765 | /** 766 | * Replace the content and snip for an entry in the clip store, identified by 767 | * its age. 768 | * 769 | * @cs: The clip store to operate on 770 | * @age: The age of the snip to replace, with 0 being the newest 771 | * @direction: Whether to iterate from the oldest to newest or vice versa 772 | * @content: The content to replace this entry with 773 | * @out_hash: Output for the generated hash, or NULL 774 | */ 775 | int cs_replace(struct clip_store *cs, enum cs_iter_direction direction, 776 | size_t age, const char *content, uint64_t *out_hash) { 777 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 778 | if (guard.status < 0) { 779 | return guard.status; 780 | } 781 | 782 | if (age >= cs->header->nr_snips) { 783 | return -ERANGE; 784 | } 785 | 786 | size_t idx = direction == CS_ITER_NEWEST_FIRST 787 | ? cs->header->nr_snips - age - 1 788 | : age; 789 | struct cs_snip *snip = cs->snips + idx; 790 | 791 | int ret = cs_content_remove(cs, snip->hash); 792 | if (ret) { 793 | return ret; 794 | } 795 | char line[CS_SNIP_LINE_SIZE]; 796 | size_t nr_lines = first_line(content, line); 797 | uint64_t hash = fnv1a_64_hash(content); 798 | cs_snip_update(snip, hash, line, nr_lines); 799 | ret = cs_content_add(cs, hash, content, CS_DUPE_KEEP_ALL); 800 | if (ret) { 801 | return ret; 802 | } 803 | if (out_hash) { 804 | *out_hash = hash; 805 | } 806 | return 0; 807 | } 808 | 809 | /** 810 | * Get the current number of entries in the clip store. 811 | * 812 | * @cs: The clip store to operate on 813 | * @out_len: Output for the length of the clip store 814 | */ 815 | int cs_len(struct clip_store *cs, size_t *out_len) { 816 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 817 | if (guard.status < 0) { 818 | return guard.status; 819 | } 820 | *out_len = cs->header->nr_snips; 821 | return 0; 822 | } 823 | -------------------------------------------------------------------------------- /src/store.h: -------------------------------------------------------------------------------- 1 | #ifndef CM_STORE_H 2 | #define CM_STORE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "util.h" 9 | 10 | #define CS_SNIP_SIZE 256 /* The size of each struct cs_snip */ 11 | #define CS_SNIP_ALLOC_BATCH 1024 /* How many snips to allocate when growing */ 12 | #define CS_HASH_STR_MAX 17 /* String length of 64bit hex + \0 */ 13 | #define PRI_HASH "%016" PRIX64 14 | 15 | /** 16 | * A single snip within the clip store. 17 | * 18 | * @hash: A 64-bit hash value associated with the content entry 19 | * @doomed: Used during cs_remove to batch mark entries for removal 20 | * @nr_lines: The number of lines in the content entry 21 | * @line: A character array containing the first salient line, terminated by a 22 | * null byte 23 | */ 24 | #define CS_SNIP_LINE_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2) - sizeof(bool) 25 | struct _packed_ cs_snip { 26 | uint64_t hash; 27 | bool doomed; 28 | uint64_t nr_lines; 29 | char line[CS_SNIP_LINE_SIZE]; 30 | }; 31 | 32 | /** 33 | * The header of the clip store. Must fit within the footprint of a regular 34 | * `cs_snip`. 35 | * 36 | * @nr_snips: The total number of snips in the clip store, excluding the 37 | * header 38 | * @nr_snips_alloc: The total number of allocated snips in the clip store 39 | * that can be used without _cs_file_resize(), excluding the 40 | * header 41 | * @_unused_padding: Padding to match the size of cs_snip 42 | */ 43 | #define CS_HEADER_PADDING_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2) 44 | struct _packed_ cs_header { 45 | uint64_t nr_snips; 46 | uint64_t nr_snips_alloc; 47 | char _unused_padding[CS_HEADER_PADDING_SIZE]; 48 | }; 49 | 50 | static_assert(sizeof(struct cs_snip) == CS_SNIP_SIZE, "cs_snip wrong size"); 51 | static_assert(sizeof(struct cs_snip) == sizeof(struct cs_header), 52 | "cs_header and cs_snip must be the same size"); 53 | 54 | /** 55 | * The main interface to the clip store for the user. 56 | * 57 | * @snip_fd: The file descriptor for the snip file 58 | * @content_dir_fd: The file descriptor for the content directory 59 | * @header: Pointer to the header in the mmapped file 60 | * @snips: Pointer to the beginning of the clip store snips in the mmapped 61 | * file, directly after the header 62 | * @ready: Indicates if the clip store is ready for operations 63 | * @refcount: The reference count for the fd flock 64 | * @local_nr_snips: Our last known header->nr_snips 65 | * @local_nr_snips_alloc: Our last known header->nr_snips_alloc 66 | */ 67 | struct clip_store { 68 | /* FDs */ 69 | int snip_fd; 70 | int content_dir_fd; 71 | 72 | /* Pointers inside mmapped snip file */ 73 | struct cs_header *header; 74 | struct cs_snip *snips; 75 | 76 | /* Synchronisation */ 77 | size_t refcount; 78 | size_t local_nr_snips; 79 | size_t local_nr_snips_alloc; 80 | bool ready; 81 | }; 82 | 83 | /** 84 | * Manages the lifecycle of a single clip store lock reference. 85 | * 86 | * @status: The result of trying update our mappings to new header values. If 87 | * less than 0, the lock should not be used. Set to -EBADF on unref. 88 | * @unref: The function to call to unref. This mostly exists to ensure that 89 | * the ref_guard is used and is not optimised by the compiler. 90 | * @cs: A pointer to the clip store structure on which the lock is held. 91 | * 92 | * Usage of this structure involves creating a ref guard instance at the 93 | * beginning of a scope where a lock is needed. Functions which need a lock 94 | * accept a `struct ref_guard *` as their first argument 95 | */ 96 | struct ref_guard { 97 | int status; 98 | struct clip_store *cs; 99 | void (*unref)(struct clip_store *); 100 | }; 101 | 102 | /** 103 | * The memory-mapped content associated with a hash in the content directory. 104 | * 105 | * @data: A pointer to the memory-mapped data 106 | * @fd: The file descriptor of the opened file from which the content is mapped 107 | * @size: The size of the mapped data 108 | */ 109 | struct cs_content { 110 | char *data; 111 | int fd; 112 | off_t size; 113 | }; 114 | 115 | /** 116 | * The direction in which to iterate over snips in the clip store. 117 | * 118 | * @CS_ITER_NEWEST_FIRST: Iterate over the snips starting from the newest. 119 | * @CS_ITER_OLDEST_FIRST: Iterate over the snips starting from the oldest. 120 | */ 121 | enum cs_iter_direction { CS_ITER_NEWEST_FIRST, CS_ITER_OLDEST_FIRST }; 122 | 123 | /** 124 | * Set the bit at position n. 125 | * 126 | * @n: The position of the bit to set 127 | */ 128 | #define BIT(n) (1UL << (n)) 129 | 130 | /** 131 | * The action to take as returned by `cs_remove`'s `should_remove()` predicate 132 | * function. 133 | * 134 | * @CS_ACTION_REMOVE: Remove this snip from the clip store 135 | * @CS_ACTION_KEEP: Keep this snip in the clip store 136 | * @CS_ACTION_STOP: Stop iteration after processing this snip 137 | * 138 | * If neither of CS_ACTION_REMOVE or CS_ACTION_KEEP are specified, the snip is 139 | * kept. 140 | */ 141 | enum cs_remove_action { 142 | CS_ACTION_REMOVE = BIT(0), 143 | CS_ACTION_KEEP = BIT(1), 144 | CS_ACTION_STOP = BIT(2), 145 | }; 146 | 147 | /** 148 | * What to do when there's a duplicate entry. 149 | * 150 | * @CS_DUPE_KEEP_ALL: Keep all duplicate entries. 151 | * @CS_DUPE_KEEP_LAST: Only keep the newest, do not insert duplicate entries. 152 | */ 153 | enum cs_dupe_policy { 154 | CS_DUPE_KEEP_ALL, 155 | CS_DUPE_KEEP_LAST, 156 | }; 157 | 158 | struct ref_guard _must_use_ _nonnull_ cs_ref(struct clip_store *cs); 159 | void _nonnull_ cs_unref(struct clip_store *cs); 160 | void _nonnull_ drop_cs_unref(struct ref_guard *guard); 161 | int _must_use_ _nonnull_ cs_destroy(struct clip_store *cs); 162 | int _must_use_ _nonnull_ cs_init(struct clip_store *cs, int snip_fd, 163 | int content_dir_fd); 164 | int _must_use_ cs_content_unmap(struct cs_content *content); 165 | void drop_cs_content_unmap(struct cs_content *content); 166 | void drop_cs_destroy(struct clip_store *cs); 167 | int _must_use_ _nonnull_ cs_content_get(struct clip_store *cs, uint64_t hash, 168 | struct cs_content *content); 169 | int _must_use_ _nonnull_n_(1) 170 | cs_make_newest(struct clip_store *cs, uint64_t hash); 171 | int _must_use_ _nonnull_n_(1) 172 | cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash, 173 | enum cs_dupe_policy dupe_policy); 174 | bool _must_use_ _nonnull_ cs_snip_iter(struct ref_guard *guard, 175 | enum cs_iter_direction direction, 176 | struct cs_snip **snip); 177 | int _must_use_ _nonnull_ cs_remove( 178 | struct clip_store *cs, enum cs_iter_direction direction, 179 | enum cs_remove_action (*should_remove)(uint64_t, const char *, void *), 180 | void *private); 181 | int _must_use_ _nonnull_ cs_trim(struct clip_store *cs, 182 | enum cs_iter_direction direction, 183 | size_t nr_keep); 184 | int _must_use_ _nonnull_n_(1, 4) 185 | cs_replace(struct clip_store *cs, enum cs_iter_direction direction, 186 | size_t age, const char *content, uint64_t *out_hash); 187 | int _nonnull_ cs_len(struct clip_store *cs, size_t *out_len); 188 | 189 | size_t _nonnull_ first_line(const char *text, char *out); 190 | 191 | #endif 192 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "store.h" 7 | #include "util.h" 8 | 9 | /** 10 | * Write data to a file descriptor, ensuring all bytes are written. 11 | */ 12 | void write_safe(int fd, const char *buf, size_t count) { 13 | while (count > 0) { 14 | ssize_t chunk_size = write(fd, buf, count); 15 | expect(chunk_size >= 0); 16 | buf += chunk_size; 17 | expect(chunk_size <= (ssize_t)count); 18 | count -= (size_t)chunk_size; 19 | } 20 | } 21 | 22 | /** 23 | * Read data from a file descriptor into a buffer safely, ensuring correct 24 | * handling of partial reads. 25 | */ 26 | size_t read_safe(int fd, char *buf, size_t count) { 27 | size_t count_start = count; 28 | while (count > 0) { 29 | ssize_t chunk_size = read(fd, buf, count); 30 | expect(chunk_size >= 0); 31 | if (chunk_size == 0) { // EOF 32 | break; 33 | } 34 | buf += chunk_size; 35 | count -= (size_t)chunk_size; 36 | } 37 | expect(count_start >= count); 38 | return count_start - count; 39 | } 40 | 41 | /** 42 | * Performs safe, bounded string formatting into a buffer. On error or 43 | * truncation, expect() aborts. 44 | */ 45 | size_t snprintf_safe(char *buf, size_t len, const char *fmt, ...) { 46 | va_list args; 47 | va_start(args, fmt); 48 | int needed = vsnprintf(buf, len, fmt, args); 49 | va_end(args); 50 | expect(needed >= 0 && (size_t)needed < len); 51 | return (size_t)needed; 52 | } 53 | 54 | /** 55 | * Runs clipserve to handle selection requests for a hash in the clip store. 56 | */ 57 | void run_clipserve(uint64_t hash) { 58 | char hash_str[CS_HASH_STR_MAX]; 59 | snprintf(hash_str, sizeof(hash_str), PRI_HASH, hash); 60 | 61 | const char *const cmd[] = {"clipserve", hash_str, NULL}; 62 | pid_t pid = fork(); 63 | expect(pid >= 0); 64 | 65 | if (pid > 0) { 66 | return; 67 | } 68 | 69 | execvp(cmd[0], (char *const *)cmd); 70 | die("Failed to exec %s: %s\n", cmd[0], strerror(errno)); 71 | } 72 | 73 | /** 74 | * Convert a positive errno value to a negative error code, ensuring a 75 | * non-zero value is returned. 76 | * 77 | * This is needed because clang-tidy and gcc may complain when doing plain 78 | * "return -errno" because the compiler does not know that errno cannot be 0 79 | * (and thus that later checks with func() == 0 cannot pass in error 80 | * situations). 81 | */ 82 | int negative_errno(void) { return errno > 0 ? -errno : -EINVAL; } 83 | 84 | /** 85 | * Convert a string to an unsigned 64-bit integer in given base, validating the 86 | * format and range of the input. 87 | */ 88 | static int str_to_uint64_base(const char *input, uint64_t *output, int base) { 89 | char *endptr; 90 | errno = 0; 91 | 92 | uint64_t val = strtoull(input, &endptr, base); 93 | if (errno > 0) { 94 | return negative_errno(); 95 | } 96 | if (!endptr || endptr == input || *endptr != 0) { 97 | return -EINVAL; 98 | } 99 | if (val != 0 && input[0] == '-') { 100 | return -ERANGE; 101 | } 102 | 103 | *output = val; 104 | return 0; 105 | } 106 | 107 | /** 108 | * Convert a string to an unsigned 64-bit integer, validating the format and 109 | * range of the input. 110 | */ 111 | int str_to_uint64(const char *input, uint64_t *output) { 112 | return str_to_uint64_base(input, output, 10); 113 | } 114 | 115 | /** 116 | * Convert a hex string to an unsigned 64-bit integer, validating the format 117 | * and range of the input. 118 | */ 119 | int str_to_hex64(const char *input, uint64_t *output) { 120 | return str_to_uint64_base(input, output, 16); 121 | } 122 | 123 | /** 124 | * Check whether debug mode is enabled and cache the result. Used for dbg(). 125 | */ 126 | bool debug_mode_enabled(void) { 127 | static int debug_enabled = -1; 128 | if (debug_enabled == -1) { 129 | const char *dbg_env = getenv("CM_DEBUG"); 130 | debug_enabled = dbg_env && streq(dbg_env, "1"); 131 | } 132 | return debug_enabled; 133 | } 134 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef CM_UTIL_H 2 | #define CM_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define _drop_(x) __attribute__((__cleanup__(drop_##x))) 12 | #define _must_use_ __attribute__((warn_unused_result)) 13 | #define _nonnull_ __attribute__((nonnull)) 14 | #define _nonnull_n_(...) __attribute__((nonnull(__VA_ARGS__))) 15 | #define _noreturn_ __attribute__((noreturn)) 16 | #define _packed_ __attribute__((packed)) 17 | #define _printf_(a, b) __attribute__((__format__(printf, a, b))) 18 | #define _unused_ __attribute__((__unused__)) 19 | #define likely(x) __builtin_expect(!!(x), 1) 20 | #define unlikely(x) __builtin_expect(!!(x), 0) 21 | #ifndef __cplusplus 22 | #define static_assert _Static_assert 23 | #endif 24 | 25 | #define UINT64_MAX_STRLEN 20 // (1 << 64) - 1 is 20 digits wide 26 | 27 | #define streq(a, b) (strcmp((a), (b)) == 0) 28 | #define strceq(a, b) (strcasecmp((a), (b)) == 0) 29 | #define strnull(s) (s) ? (s) : "[null]" 30 | 31 | #define arrlen(x) \ 32 | (__builtin_choose_expr( \ 33 | !__builtin_types_compatible_p(typeof(x), typeof(&*(x))), \ 34 | sizeof(x) / sizeof((x)[0]), (void)0 /* decayed, compile error */)) 35 | 36 | #define _die(dump, fmt, ...) \ 37 | do { \ 38 | fprintf(stderr, "FATAL: " fmt, ##__VA_ARGS__); \ 39 | if (dump) { \ 40 | abort(); \ 41 | } \ 42 | exit(1); \ 43 | } while (0) 44 | #define die(fmt, ...) _die(0, fmt, ##__VA_ARGS__) 45 | #define die_on(cond, fmt, ...) \ 46 | do { \ 47 | if (unlikely(cond)) { \ 48 | die(fmt, ##__VA_ARGS__); \ 49 | } \ 50 | } while (0) 51 | #define expect(x) \ 52 | do { \ 53 | if (!likely(x)) { \ 54 | _die(1, "!(%s) at %s:%s:%d\n", #x, __FILE__, __func__, __LINE__); \ 55 | } \ 56 | } while (0) 57 | 58 | #define dbg(fmt, ...) \ 59 | do { \ 60 | if (debug_mode_enabled()) { \ 61 | fprintf(stderr, "%s:%ld:%s:%s:%d: " fmt, prog_name, \ 62 | (long)getpid(), __FILE__, __func__, __LINE__, \ 63 | ##__VA_ARGS__); \ 64 | } \ 65 | } while (0) 66 | 67 | void _nonnull_ write_safe(int fd, const char *buf, size_t count); 68 | size_t _nonnull_ read_safe(int fd, char *buf, size_t count); 69 | size_t _printf_(3, 4) 70 | snprintf_safe(char *buf, size_t len, const char *fmt, ...); 71 | 72 | void run_clipserve(uint64_t hash); 73 | 74 | /** 75 | * __attribute__((cleanup)) functions 76 | */ 77 | #define DEFINE_DROP_FUNC_PTR(type, func) \ 78 | static inline void drop_##func(type *p) { func(p); } 79 | #define DEFINE_DROP_FUNC(type, func) \ 80 | static inline void drop_##func(type *p) { \ 81 | if (*p) \ 82 | func(*p); \ 83 | } 84 | #define DEFINE_DROP_FUNC_VOID(func) \ 85 | static inline void drop_##func(void *p) { \ 86 | void **pp = p; \ 87 | if (*pp) \ 88 | func(*pp); \ 89 | } 90 | 91 | static inline void drop_close(int *fd) { 92 | if (*fd >= 0) { 93 | close(*fd); 94 | } 95 | } 96 | 97 | DEFINE_DROP_FUNC_VOID(free) 98 | DEFINE_DROP_FUNC(FILE *, fclose) 99 | DEFINE_DROP_FUNC(DIR *, closedir) 100 | 101 | int _must_use_ negative_errno(void); 102 | int _nonnull_ str_to_uint64(const char *input, uint64_t *output); 103 | int _nonnull_ str_to_hex64(const char *input, uint64_t *output); 104 | bool debug_mode_enabled(void); 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /src/x.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "x.h" 6 | 7 | /** 8 | * Fetch the title of the window with the specified window ID. 9 | */ 10 | char *get_window_title(Display *dpy, Window owner) { 11 | Atom props[] = {XInternAtom(dpy, "_NET_WM_NAME", False), XA_WM_NAME}; 12 | Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); 13 | Atom actual_type; 14 | int format; 15 | unsigned long nr_items, bytes_after; 16 | unsigned char *prop = NULL; 17 | 18 | for (size_t i = 0; i < arrlen(props); i++) { 19 | if (XGetWindowProperty(dpy, owner, props[i], 0, (~0L), False, 20 | (props[i] == XA_WM_NAME) ? AnyPropertyType 21 | : utf8_string, 22 | &actual_type, &format, &nr_items, &bytes_after, 23 | &prop) == Success && 24 | prop) { 25 | return (char *)prop; 26 | } 27 | } 28 | return NULL; 29 | } 30 | 31 | /** 32 | * Certain X11 operations may fail in expected ways. For example, when 33 | * attempting to interact with a window that has been closed. This handler 34 | * avoids the application terminating in such cases. 35 | * 36 | * The cppcheck suppression is for a false positive: this is a callback and 37 | * cannot be changed. 38 | */ 39 | // cppcheck-suppress [constParameterPointer,unmatchedSuppression] 40 | int xerror_handler(Display *dpy _unused_, XErrorEvent *ee) { 41 | if (ee->error_code == BadWindow || 42 | (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) || 43 | (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) || 44 | (ee->request_code == X_PolyFillRectangle && 45 | ee->error_code == BadDrawable) || 46 | (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) || 47 | (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) || 48 | (ee->request_code == X_GrabButton && ee->error_code == BadAccess) || 49 | (ee->request_code == X_GrabKey && ee->error_code == BadAccess) || 50 | (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) { 51 | return 0; 52 | } 53 | die("X error with request code=%d, error code=%d\n", ee->request_code, 54 | ee->error_code); 55 | } 56 | 57 | #define FALLBACK_CHUNK_BYTES 4 * 1024 58 | 59 | /** 60 | * Calculate and cache an appropriate INCR chunk size. 61 | * 62 | * We consider selections larger than a quarter of the maximum request size to 63 | * be "large". That's what others (like xclip) do, so it's clearly ok in 64 | * practice. 65 | */ 66 | size_t get_chunk_size(Display *dpy) { 67 | // Units are 4-byte words, so this is 1/4 in bytes 68 | size_t chunk_size = XExtendedMaxRequestSize(dpy); 69 | if (chunk_size == 0) { 70 | chunk_size = XMaxRequestSize(dpy); 71 | } 72 | return chunk_size ? chunk_size / 4 : FALLBACK_CHUNK_BYTES; 73 | } 74 | 75 | /** 76 | * Add a new INCR transfer to the active list. 77 | */ 78 | void it_add(struct incr_transfer **it_list, struct incr_transfer *it) { 79 | if (*it_list) { 80 | (*it_list)->prev = it; 81 | } 82 | it->next = *it_list; 83 | it->prev = NULL; 84 | *it_list = it; 85 | } 86 | /** 87 | * Remove an INCR transfer from the active list. 88 | */ 89 | void it_remove(struct incr_transfer **it_list, struct incr_transfer *it) { 90 | if (it->prev) { 91 | it->prev->next = it->next; 92 | } 93 | if (it->next) { 94 | it->next->prev = it->prev; 95 | } 96 | if (*it_list == it) { 97 | *it_list = it->next; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/x.h: -------------------------------------------------------------------------------- 1 | #ifndef CM_X_H 2 | #define CM_X_H 3 | 4 | #include 5 | 6 | #include "util.h" 7 | 8 | DEFINE_DROP_FUNC_VOID(XFree) 9 | 10 | size_t _nonnull_ get_chunk_size(Display *dpy); 11 | char _nonnull_ *get_window_title(Display *dpy, Window owner); 12 | int xerror_handler(Display *dpy _unused_, XErrorEvent *ee); 13 | 14 | struct incr_transfer { 15 | struct incr_transfer *next; 16 | struct incr_transfer *prev; 17 | Window requestor; 18 | Atom property; 19 | Atom target; 20 | int format; 21 | char *data; 22 | size_t data_size; 23 | size_t data_capacity; 24 | size_t offset; 25 | }; 26 | 27 | #define it_dbg(it, fmt, ...) \ 28 | dbg("[incr 0x%lx] " fmt, (unsigned long)(it)->requestor, ##__VA_ARGS__) 29 | void _nonnull_ it_add(struct incr_transfer **it_list, struct incr_transfer *it); 30 | void _nonnull_ it_remove(struct incr_transfer **it_list, 31 | struct incr_transfer *it); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /tests/test_store.c: -------------------------------------------------------------------------------- 1 | #undef NDEBUG 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "../src/store.h" 18 | #include "../src/util.h" 19 | 20 | #define COL_NORMAL "\x1B[0m" 21 | #define COL_GREEN "\x1B[32m" 22 | #define COL_RED "\x1B[31m" 23 | 24 | #define t_assert(test) \ 25 | do { \ 26 | if (!(test)) { \ 27 | printf(" %s[FAIL:%d] %s%s\n", COL_RED, __LINE__, #test, \ 28 | COL_NORMAL); \ 29 | return false; \ 30 | } \ 31 | printf(" %s[PASS:%d] %s%s\n", COL_GREEN, __LINE__, #test, \ 32 | COL_NORMAL); \ 33 | } while (0) 34 | 35 | #define t_run(test) \ 36 | do { \ 37 | printf("%s:\n", #test); \ 38 | bool ret = test(); \ 39 | printf("\n"); \ 40 | if (!ret) { \ 41 | return ret; \ 42 | } \ 43 | } while (0) 44 | 45 | #define TEST_SNIP_FILE "/clip_store_snip_test" 46 | #define TEST_CONTENT_DIR "/dev/shm/clip_store_content_dir_test" 47 | 48 | static int create_test_snip_fd(void) { 49 | shm_unlink(TEST_SNIP_FILE); 50 | int snip_fd = shm_open(TEST_SNIP_FILE, O_RDWR | O_CREAT | O_EXCL, 0600); 51 | assert(snip_fd >= 0); 52 | return snip_fd; 53 | } 54 | 55 | static void drop_remove_test_snip_fd(int *snip_fd) { 56 | close(*snip_fd); 57 | int ret = shm_unlink(TEST_SNIP_FILE); 58 | assert(ret == 0); 59 | } 60 | 61 | static void _drop_remove_test_content_dir_fd(int *dir_fd_ptr) { 62 | int dir_fd = *dir_fd_ptr; 63 | DIR *dir = fdopendir(dir_fd); 64 | if (dir == NULL) { 65 | close(dir_fd); 66 | return; 67 | } 68 | 69 | struct dirent *entry; 70 | while ((entry = readdir(dir)) != NULL) { 71 | if (streq(entry->d_name, ".") || streq(entry->d_name, "..")) { 72 | continue; 73 | } 74 | 75 | if (entry->d_type == DT_REG) { 76 | int ret = unlinkat(dir_fd, entry->d_name, 0); 77 | assert(ret == 0); 78 | } else if (entry->d_type == DT_DIR) { 79 | int subdir_fd = 80 | openat(dir_fd, entry->d_name, O_RDONLY | O_DIRECTORY); 81 | assert(subdir_fd >= 0); 82 | _drop_remove_test_content_dir_fd(&subdir_fd); 83 | int ret = unlinkat(dir_fd, entry->d_name, AT_REMOVEDIR); 84 | assert(ret == 0); 85 | } 86 | } 87 | 88 | closedir(dir); 89 | } 90 | 91 | static void drop_remove_test_content_dir_fd(int *dir_fd_ptr) { 92 | _drop_remove_test_content_dir_fd(dir_fd_ptr); 93 | int ret = rmdir(TEST_CONTENT_DIR); 94 | assert(ret == 0); 95 | } 96 | 97 | static void remove_test_content_dir(const char *path) { 98 | int dir_fd = open(path, O_RDONLY); 99 | if (dir_fd < 0) { 100 | assert(errno == ENOENT); 101 | return; 102 | } 103 | drop_remove_test_content_dir_fd(&dir_fd); 104 | } 105 | 106 | static int create_test_content_dir_fd(void) { 107 | remove_test_content_dir(TEST_CONTENT_DIR); 108 | int ret = mkdir(TEST_CONTENT_DIR, 0700); 109 | assert(ret == 0); 110 | int dir_fd = open(TEST_CONTENT_DIR, O_RDONLY); 111 | assert(dir_fd >= 0); 112 | return dir_fd; 113 | } 114 | 115 | /* Test callables */ 116 | static enum cs_remove_action remove_if_five(uint64_t hash, const char *line, 117 | void *private) { 118 | (void)hash; 119 | size_t *count = private; 120 | enum cs_remove_action ret = 0; 121 | 122 | if (*count == 0) { 123 | ret |= CS_ACTION_STOP; 124 | } 125 | (*count)--; 126 | 127 | if (line[0] == '5') { 128 | ret |= CS_ACTION_REMOVE; 129 | } 130 | 131 | return ret; 132 | } 133 | 134 | static struct clip_store setup_test(void) { 135 | struct clip_store cs; 136 | int snip_fd = create_test_snip_fd(); 137 | int content_dir_fd = create_test_content_dir_fd(); 138 | int ret = cs_init(&cs, snip_fd, content_dir_fd); 139 | assert(ret == 0); 140 | return cs; 141 | } 142 | 143 | static void drop_teardown_test(struct clip_store *cs) { 144 | int snip_fd = cs->snip_fd; 145 | int content_dir_fd = cs->content_dir_fd; 146 | int ret = cs_destroy(cs); 147 | assert(ret == 0); 148 | close(snip_fd); 149 | shm_unlink(TEST_SNIP_FILE); 150 | close(content_dir_fd); 151 | remove_test_content_dir(TEST_CONTENT_DIR); 152 | } 153 | 154 | static void add_ten_snips(struct clip_store *cs) { 155 | for (char i = 0; i < 10; i++) { 156 | char num[8]; 157 | snprintf(num, sizeof(num), "%d", i); 158 | int ret = cs_add(cs, num, NULL, CS_DUPE_KEEP_ALL); 159 | assert(ret == 0); 160 | } 161 | } 162 | 163 | /* Tests */ 164 | 165 | static bool test__cs_init(void) { 166 | _drop_(teardown_test) struct clip_store cs = setup_test(); 167 | 168 | t_assert(cs.snip_fd >= 0); 169 | t_assert(cs.content_dir_fd >= 0); 170 | 171 | /* Check header fields were set up and are correct */ 172 | struct stat st; 173 | t_assert(fstat(cs.snip_fd, &st) == 0); 174 | t_assert(st.st_size == CS_SNIP_SIZE); 175 | t_assert(cs.header->nr_snips_alloc == 0); 176 | 177 | return true; 178 | } 179 | 180 | static bool test__cs_init__bad_size(void) { 181 | _drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd(); 182 | _drop_(remove_test_content_dir_fd) int content_dir_fd = 183 | create_test_content_dir_fd(); 184 | t_assert(ftruncate(snip_fd, CS_SNIP_SIZE - 1) == 0); 185 | struct clip_store cs; 186 | t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL); 187 | 188 | return true; 189 | } 190 | 191 | static bool test__cs_init__bad_size_aligned(void) { 192 | _drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd(); 193 | _drop_(remove_test_content_dir_fd) int content_dir_fd = 194 | create_test_content_dir_fd(); 195 | t_assert(ftruncate(snip_fd, CS_SNIP_SIZE * 2) == 0); 196 | struct clip_store cs; 197 | t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL); 198 | 199 | return true; 200 | } 201 | 202 | static bool test__cs_add(void) { 203 | _drop_(teardown_test) struct clip_store cs = setup_test(); 204 | 205 | for (char i = 0; i < 10; i++) { 206 | char num[8]; 207 | snprintf(num, sizeof(num), "%d", i); 208 | 209 | uint64_t hash; 210 | int ret = cs_add(&cs, num, &hash, CS_DUPE_KEEP_ALL); 211 | t_assert(ret == 0); 212 | 213 | _drop_(cs_content_unmap) struct cs_content content; 214 | ret = cs_content_get(&cs, hash, &content); 215 | t_assert(ret == 0); 216 | t_assert(strncmp(content.data, num, content.size) == 0); 217 | } 218 | 219 | t_assert(cs.header->nr_snips == 10); 220 | 221 | return true; 222 | } 223 | 224 | static bool test__cs_snip_iter(void) { 225 | _drop_(teardown_test) struct clip_store cs = setup_test(); 226 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 227 | 228 | add_ten_snips(&cs); 229 | 230 | struct cs_snip *snip = NULL; 231 | int last_num = 9; 232 | while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { 233 | _drop_(cs_content_unmap) struct cs_content content; 234 | int ret = cs_content_get(&cs, snip->hash, &content); 235 | t_assert(ret == 0); 236 | 237 | /* add_ten_snips adds from 0-9, we should get the last one first */ 238 | int num = content.data[0] - '0'; 239 | t_assert(num == last_num--); 240 | } 241 | 242 | snip = NULL; 243 | last_num = 0; 244 | while (cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip)) { 245 | _drop_(cs_content_unmap) struct cs_content content; 246 | int ret = cs_content_get(&cs, snip->hash, &content); 247 | t_assert(ret == 0); 248 | 249 | int num = content.data[0] - '0'; 250 | t_assert(num == last_num++); 251 | } 252 | 253 | return true; 254 | } 255 | 256 | static bool test__cs_remove(void) { 257 | _drop_(teardown_test) struct clip_store cs = setup_test(); 258 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 259 | 260 | add_ten_snips(&cs); 261 | 262 | size_t nr_iter = 1; 263 | t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) == 264 | 0); 265 | t_assert(cs.header->nr_snips == 10); 266 | nr_iter = SIZE_MAX; 267 | t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) == 268 | 0); 269 | t_assert(cs.header->nr_snips == 9); 270 | 271 | return true; 272 | } 273 | 274 | static bool test__cs_trim(void) { 275 | _drop_(teardown_test) struct clip_store cs = setup_test(); 276 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 277 | 278 | add_ten_snips(&cs); 279 | 280 | struct cs_snip *snip = NULL; 281 | while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) 282 | ; 283 | uint64_t oldest_hash = snip->hash; 284 | 285 | t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 3) == 0); 286 | t_assert(cs.header->nr_snips == 3); 287 | 288 | /* Check we kept the most recently added ones */ 289 | int last_num = 9; 290 | snip = NULL; 291 | while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { 292 | _drop_(cs_content_unmap) struct cs_content content; 293 | int ret = cs_content_get(&cs, snip->hash, &content); 294 | t_assert(ret == 0); 295 | 296 | int num = content.data[0] - '0'; 297 | t_assert(num == last_num--); 298 | } 299 | 300 | /* Check the oldest snip hash is gone */ 301 | _drop_(cs_content_unmap) struct cs_content content; 302 | t_assert(cs_content_get(&cs, oldest_hash, &content) == -ENOENT); 303 | 304 | return true; 305 | } 306 | 307 | static bool test__cs_replace(void) { 308 | _drop_(teardown_test) struct clip_store cs = setup_test(); 309 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 310 | 311 | add_ten_snips(&cs); 312 | 313 | const char *new = "new"; 314 | 315 | int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 1, new, NULL); 316 | t_assert(ret == 0); 317 | 318 | struct cs_snip *snip = NULL; 319 | bool i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); 320 | t_assert(i_ret == true); 321 | t_assert(streq(snip->line, "9")); 322 | i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); 323 | t_assert(i_ret == true); 324 | t_assert(streq(snip->line, new)); 325 | 326 | _drop_(cs_content_unmap) struct cs_content first_content; 327 | ret = cs_content_get(&cs, snip->hash, &first_content); 328 | t_assert(ret == 0); 329 | t_assert(strncmp(first_content.data, new, first_content.size) == 0); 330 | 331 | /* No other clips should be affected */ 332 | int last_num = 7; 333 | while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) { 334 | _drop_(cs_content_unmap) struct cs_content content; 335 | ret = cs_content_get(&cs, snip->hash, &content); 336 | t_assert(ret == 0); 337 | 338 | int num = content.data[0] - '0'; 339 | t_assert(num == last_num--); 340 | } 341 | 342 | return true; 343 | } 344 | 345 | static bool test__reuse_cs(void) { 346 | _drop_(teardown_test) struct clip_store cs = setup_test(); 347 | 348 | add_ten_snips(&cs); 349 | t_assert(cs_destroy(&cs) == 0); 350 | 351 | t_assert(cs_init(&cs, cs.snip_fd, cs.content_dir_fd) == 0); 352 | t_assert(cs.header->nr_snips == 10); 353 | 354 | return true; 355 | } 356 | 357 | static bool test__cs_add__exceeds_snip_line_size(void) { 358 | _drop_(teardown_test) struct clip_store cs = setup_test(); 359 | 360 | /* Construct a string that exceeds CS_SNIP_LINE_SIZE */ 361 | char long_content[CS_SNIP_LINE_SIZE + 100]; 362 | memset(long_content, 'A', sizeof(long_content)); 363 | long_content[sizeof(long_content) - 1] = '\0'; 364 | 365 | int ret = cs_add(&cs, long_content, NULL, CS_DUPE_KEEP_ALL); 366 | t_assert(ret == 0); 367 | 368 | struct cs_snip *snip = NULL; 369 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 370 | bool found = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); 371 | t_assert(found); 372 | t_assert(strlen(snip->line) == CS_SNIP_LINE_SIZE - 1); 373 | 374 | return true; 375 | } 376 | 377 | static bool test__cs_trim__when_empty(void) { 378 | _drop_(teardown_test) struct clip_store cs = setup_test(); 379 | 380 | int ret = cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0); 381 | t_assert(ret == 0); 382 | t_assert(cs.header->nr_snips == 0); 383 | 384 | return true; 385 | } 386 | 387 | static bool test__cs_remove___empty(void) { 388 | _drop_(teardown_test) struct clip_store cs = setup_test(); 389 | 390 | /* Attempt to remove any entry, if present */ 391 | size_t dummy = 0; 392 | int ret = cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &dummy); 393 | t_assert(ret == 0); 394 | t_assert(cs.header->nr_snips == 0); 395 | 396 | return true; 397 | } 398 | 399 | static bool test__cs_add__around_alloc_batch_threshold(void) { 400 | _drop_(teardown_test) struct clip_store cs = setup_test(); 401 | 402 | for (size_t i = 0; i < CS_SNIP_ALLOC_BATCH - 1; i++) { 403 | int ret = cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL); 404 | assert(ret == 0); 405 | } 406 | t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH - 1); 407 | 408 | /* Add one more entry to exceed the batch threshold */ 409 | t_assert(cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL) == 0); 410 | t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH); 411 | t_assert(cs.header->nr_snips_alloc >= CS_SNIP_ALLOC_BATCH); 412 | 413 | return true; 414 | } 415 | 416 | static bool test__cs_trim__no_remove_when_still_referenced(void) { 417 | _drop_(teardown_test) struct clip_store cs = setup_test(); 418 | 419 | uint64_t hash; 420 | for (size_t i = 0; i < 2; i++) { 421 | int ret = cs_add(&cs, "test content", &hash, CS_DUPE_KEEP_ALL); 422 | t_assert(ret == 0); 423 | } 424 | 425 | _drop_(cs_content_unmap) struct cs_content content; 426 | 427 | t_assert(cs.header->nr_snips == 2); 428 | t_assert(cs_content_get(&cs, hash, &content) == 0); 429 | 430 | t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 1) == 0); 431 | t_assert(cs.header->nr_snips == 1); 432 | t_assert(cs_content_get(&cs, hash, &content) == 0); 433 | 434 | t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0) == 0); 435 | t_assert(cs.header->nr_snips == 0); 436 | t_assert(cs_content_get(&cs, hash, &content) == -ENOENT); 437 | 438 | return true; 439 | } 440 | 441 | static bool test__cs_replace__out_of_bounds(void) { 442 | _drop_(teardown_test) struct clip_store cs = setup_test(); 443 | 444 | add_ten_snips(&cs); 445 | 446 | int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 10, "test content", NULL); 447 | t_assert(ret == -ERANGE); 448 | 449 | return true; 450 | } 451 | 452 | static bool test__cs_snip__correct_nr_lines(void) { 453 | _drop_(teardown_test) struct clip_store cs = setup_test(); 454 | 455 | add_ten_snips(&cs); 456 | 457 | /* No need to do exhaustive ones, they're done in test__first_line_* */ 458 | uint64_t hash; 459 | struct cs_snip *snip = NULL; 460 | int ret = 461 | cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, "one\ntwo\nthree", &hash); 462 | 463 | t_assert(ret == 0); 464 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 465 | t_assert(cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)); 466 | t_assert(snip->hash == hash); 467 | t_assert(snip->nr_lines == 3); 468 | 469 | return true; 470 | } 471 | 472 | static bool test__first_line__empty(void) { 473 | char line[CS_SNIP_LINE_SIZE]; 474 | size_t num_lines = first_line("", line); 475 | printf("%s\n", line); 476 | t_assert(streq(line, "")); 477 | t_assert(num_lines == 0); 478 | num_lines = first_line("\n", line); 479 | t_assert(streq(line, "")); 480 | t_assert(num_lines == 1); 481 | 482 | num_lines = first_line("\n\n\n", line); 483 | t_assert(streq(line, "")); 484 | t_assert(num_lines == 3); 485 | return true; 486 | } 487 | 488 | static bool test__first_line__only_one(void) { 489 | char line[CS_SNIP_LINE_SIZE]; 490 | size_t num_lines = first_line("Foo bar\n", line); 491 | t_assert(streq(line, "Foo bar")); 492 | t_assert(num_lines == 1); 493 | return true; 494 | } 495 | 496 | static bool test__first_line__multiple(void) { 497 | char line[CS_SNIP_LINE_SIZE]; 498 | size_t num_lines = first_line("Foo bar\nbaz\nqux\n", line); 499 | t_assert(streq(line, "Foo bar")); 500 | t_assert(num_lines == 3); 501 | return true; 502 | } 503 | 504 | static bool test__first_line__no_final_newline(void) { 505 | /* If the last line didn't end with a newline, still count it */ 506 | char line[CS_SNIP_LINE_SIZE]; 507 | size_t num_lines = first_line("Foo bar", line); 508 | t_assert(streq(line, "Foo bar")); 509 | t_assert(num_lines == 1); 510 | num_lines = first_line("Foo bar\nbaz", line); 511 | t_assert(streq(line, "Foo bar")); 512 | t_assert(num_lines == 2); 513 | return true; 514 | } 515 | 516 | static bool test__first_line__ignore_blank_lines(void) { 517 | char line[CS_SNIP_LINE_SIZE]; 518 | size_t num_lines = first_line("\n\n\nFoo bar\n\n\n", line); 519 | t_assert(streq(line, "Foo bar")); 520 | t_assert(num_lines == 6); 521 | return true; 522 | } 523 | 524 | static bool test__first_line__unicode(void) { 525 | char line[CS_SNIP_LINE_SIZE]; 526 | size_t num_lines = first_line("道", line); 527 | t_assert(streq(line, "道")); 528 | t_assert(num_lines == 1); 529 | num_lines = first_line("道\n", line); 530 | t_assert(streq(line, "道")); 531 | t_assert(num_lines == 1); 532 | num_lines = first_line("道\n非", line); 533 | t_assert(streq(line, "道")); 534 | t_assert(num_lines == 2); 535 | return true; 536 | } 537 | 538 | static bool test__synchronisation(void) { 539 | _drop_(remove_test_snip_fd) int snip_fd1 = create_test_snip_fd(); 540 | _drop_(remove_test_content_dir_fd) int content_dir_fd1 = 541 | create_test_content_dir_fd(); 542 | _drop_(close) int snip_fd2 = dup(snip_fd1); 543 | _drop_(close) int content_dir_fd2 = dup(content_dir_fd1); 544 | 545 | assert(snip_fd2 >= 0 && content_dir_fd2 >= 0); 546 | 547 | struct clip_store cs1, cs2; 548 | int ret = cs_init(&cs1, snip_fd1, content_dir_fd1); 549 | t_assert(ret == 0); 550 | ret = cs_init(&cs2, snip_fd2, content_dir_fd2); 551 | t_assert(ret == 0); 552 | 553 | uint64_t hash; 554 | ret = cs_add(&cs1, "test content", &hash, CS_DUPE_KEEP_ALL); 555 | t_assert(ret == 0); 556 | 557 | bool found = false; 558 | struct cs_snip *snip = NULL; 559 | _drop_(cs_unref) struct ref_guard guard_cs2 = cs_ref(&cs2); 560 | while (cs_snip_iter(&guard_cs2, CS_ITER_NEWEST_FIRST, &snip)) { 561 | if (snip->hash == hash) { 562 | found = true; 563 | break; 564 | } 565 | } 566 | t_assert(found); 567 | 568 | ret = cs_trim(&cs2, CS_ITER_NEWEST_FIRST, 0); 569 | t_assert(ret == 0); 570 | 571 | found = false; 572 | snip = NULL; 573 | _drop_(cs_unref) struct ref_guard guard_cs1 = cs_ref(&cs1); 574 | while (cs_snip_iter(&guard_cs1, CS_ITER_NEWEST_FIRST, &snip)) { 575 | found = true; 576 | } 577 | t_assert(!found); 578 | 579 | t_assert(cs_destroy(&cs1) == 0); 580 | t_assert(cs_destroy(&cs2) == 0); 581 | 582 | return true; 583 | } 584 | 585 | static bool test__cs_add__dupe_keep_all(void) { 586 | _drop_(teardown_test) struct clip_store cs = setup_test(); 587 | 588 | uint64_t hash1, hash2; 589 | int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_ALL); 590 | t_assert(ret == 0); 591 | ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_ALL); 592 | t_assert(ret == 0); 593 | t_assert(hash1 == hash2); 594 | t_assert(cs.header->nr_snips == 2); 595 | 596 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 597 | struct cs_snip *snip = NULL; 598 | bool iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip); 599 | t_assert(iter_ret == true); 600 | t_assert(snip->hash == hash1); 601 | iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip); 602 | t_assert(iter_ret == true); 603 | t_assert(snip->hash == hash2); 604 | 605 | return true; 606 | } 607 | 608 | static bool test__cs_add__dupe_keep_last(void) { 609 | _drop_(teardown_test) struct clip_store cs = setup_test(); 610 | 611 | uint64_t hash1, hash2, hash3; 612 | int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_LAST); 613 | t_assert(ret == 0); 614 | t_assert(cs.header->nr_snips == 1); 615 | ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_LAST); 616 | t_assert(ret == 0); 617 | t_assert(cs.header->nr_snips == 1); 618 | ret = cs_add(&cs, "duplicate", &hash3, CS_DUPE_KEEP_LAST); 619 | t_assert(ret == 0); 620 | t_assert(cs.header->nr_snips == 1); 621 | t_assert(hash1 == hash2); 622 | t_assert(hash1 == hash3); 623 | 624 | return true; 625 | } 626 | 627 | /* After adding a duplicate entry, ensure the duplicate is moved to the newest 628 | * slot while other entries remain in order. */ 629 | static bool test__cs_add__dupe_keep_last_with_multiple_entries(void) { 630 | _drop_(teardown_test) struct clip_store cs = setup_test(); 631 | 632 | uint64_t hash_a, hash_dup; 633 | int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL); 634 | t_assert(ret == 0); 635 | ret = cs_add(&cs, "duplicate", &hash_dup, CS_DUPE_KEEP_ALL); 636 | t_assert(ret == 0); 637 | ret = cs_add(&cs, "B", NULL, CS_DUPE_KEEP_ALL); 638 | t_assert(ret == 0); 639 | t_assert(cs.header->nr_snips == 3); 640 | /* Now add a duplicate entry with KEEP_LAST which should move the duplicate 641 | * to the newest slot */ 642 | ret = cs_add(&cs, "duplicate", NULL, CS_DUPE_KEEP_LAST); 643 | t_assert(ret == 0); 644 | t_assert(cs.header->nr_snips == 3); 645 | _drop_(cs_unref) struct ref_guard guard = cs_ref(&cs); 646 | struct cs_snip *snip = NULL; 647 | bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); 648 | t_assert(iter_ret == true); 649 | t_assert(snip->hash == hash_dup); 650 | 651 | return true; 652 | } 653 | 654 | static bool check_order(struct clip_store *cs, uint64_t *order, size_t len) { 655 | _drop_(cs_unref) struct ref_guard guard = cs_ref(cs); 656 | struct cs_snip *snip = NULL; 657 | for (size_t i = 0; i < len; ++i) { 658 | bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip); 659 | t_assert(iter_ret == true); 660 | t_assert(snip->hash == order[i]); 661 | } 662 | return 0; 663 | } 664 | 665 | /* After calling cs_make_newest make sure the entry is at the newest slot while 666 | * other entries remain in order. */ 667 | static bool test__cs_make_newest(void) { 668 | _drop_(teardown_test) struct clip_store cs = setup_test(); 669 | 670 | uint64_t hash_a, hash_b, hash_c; 671 | int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL); 672 | t_assert(ret == 0); 673 | ret = cs_add(&cs, "B", &hash_b, CS_DUPE_KEEP_ALL); 674 | t_assert(ret == 0); 675 | ret = cs_add(&cs, "C", &hash_c, CS_DUPE_KEEP_ALL); 676 | t_assert(ret == 0); 677 | t_assert(cs.header->nr_snips == 3); 678 | 679 | uint64_t order_before[3] = { hash_c, hash_b, hash_a }; 680 | ret = check_order(&cs, order_before, 3); 681 | t_assert(ret == 0); 682 | /* Now the order should change to ["A", "C", "B"] */ 683 | ret = cs_make_newest(&cs, hash_a); 684 | t_assert(ret == 0); 685 | uint64_t order_after[3] = { hash_a, hash_c, hash_b }; 686 | ret = check_order(&cs, order_after, 3); 687 | t_assert(ret == 0); 688 | 689 | return true; 690 | } 691 | 692 | int main(void) { 693 | t_run(test__cs_init); 694 | t_run(test__cs_init__bad_size); 695 | t_run(test__cs_init__bad_size_aligned); 696 | t_run(test__cs_add); 697 | t_run(test__cs_snip_iter); 698 | t_run(test__cs_remove); 699 | t_run(test__cs_trim); 700 | t_run(test__cs_replace); 701 | t_run(test__reuse_cs); 702 | t_run(test__cs_add__exceeds_snip_line_size); 703 | t_run(test__cs_trim__when_empty); 704 | t_run(test__cs_remove___empty); 705 | t_run(test__cs_add__around_alloc_batch_threshold); 706 | t_run(test__cs_replace__out_of_bounds); 707 | t_run(test__synchronisation); 708 | t_run(test__cs_trim__no_remove_when_still_referenced); 709 | t_run(test__cs_snip__correct_nr_lines); 710 | t_run(test__first_line__empty); 711 | t_run(test__first_line__only_one); 712 | t_run(test__first_line__multiple); 713 | t_run(test__first_line__no_final_newline); 714 | t_run(test__first_line__ignore_blank_lines); 715 | t_run(test__first_line__unicode); 716 | t_run(test__cs_add__dupe_keep_all); 717 | t_run(test__cs_add__dupe_keep_last); 718 | t_run(test__cs_add__dupe_keep_last_with_multiple_entries); 719 | t_run(test__cs_make_newest); 720 | 721 | return 0; 722 | } 723 | -------------------------------------------------------------------------------- /tests/x_integration_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exE -o errtrace 4 | 5 | dump_cache() { 6 | printf '\nTest failure: contents of cache:\n\n' >&2 7 | cat "$l_out" >&2 8 | } 9 | 10 | if ! (( NO_PID_NAMESPACE )) && (( EUID )); then 11 | export _UNSHARED=1 12 | exec unshare --user --map-root-user --pid --mount --fork --mount-proc "$0" "$@" 13 | fi 14 | 15 | if (( _UNSHARED )); then 16 | # Get our own tmp 17 | mount -t tmpfs unshared_tmp /tmp 18 | fi 19 | 20 | cd "${0%/*}"/.. 21 | make clean debug 22 | cd src 23 | 24 | export PATH=$PWD:$PATH 25 | export CM_CONFIG=$(mktemp) 26 | export CM_DIR=$(mktemp -d) 27 | export CM_DEBUG=1 28 | export ASAN_OPTIONS='halt_on_error=1:abort_on_error=1' 29 | export UBSAN_OPTIONS='halt_on_error=1:abort_on_error=1' 30 | 31 | launcher=$(mktemp) 32 | l_out=$(mktemp) 33 | 34 | trap dump_cache ERR 35 | 36 | cat > "$launcher" << EOF 37 | #!/bin/bash 38 | 39 | cat > "$l_out" 40 | 41 | if [[ \$SELECT ]]; then 42 | printf '%s\\n' "\$SELECT" 43 | fi 44 | EOF 45 | chmod a+x "$launcher" 46 | export CM_LAUNCHER="$launcher" 47 | 48 | kill_background_jobs() { 49 | local -a bg 50 | readarray -t bg < <(jobs -p) 51 | (( ${#bg[@]} )) && kill -- "${bg[@]}" 2>/dev/null 52 | } 53 | 54 | primary() { 55 | printf '%s' "${1?}" | xsel -p 56 | } 57 | 58 | trap 'kill_background_jobs' EXIT 59 | 60 | settle() { 61 | sleep 0.2 62 | } 63 | 64 | long_settle() { 65 | sleep 2 66 | } 67 | 68 | check_nr_clips() { 69 | if [[ $SELECT ]]; then 70 | clipmenu 71 | else 72 | # On no selection, we exit 1. Avoid masking real problems by only doing 73 | # it when no selection is specified. 74 | clipmenu || true 75 | fi 76 | (( $(wc -l < "$l_out") == "${1?}" )) 77 | } 78 | 79 | if ! (( USE_CURRENT_DISPLAY )); then 80 | export DISPLAY=:1911 81 | Xvfb "$DISPLAY" & 82 | sleep 2 83 | fi 84 | 85 | # Clear selections 86 | xsel -bc 87 | xsel -pc 88 | xsel -sc 89 | 90 | clipmenud & 91 | settle 92 | 93 | # Should be empty 94 | check_nr_clips 0 95 | 96 | primary foo 97 | settle 98 | 99 | check_nr_clips 1 100 | 101 | # Put the same one repeatedly, should ignore it 102 | primary foo 103 | primary foo 104 | primary foo 105 | settle 106 | 107 | settle 108 | check_nr_clips 1 109 | [[ "$(< "$l_out")" == "[1] foo" ]] 110 | 111 | # Put possible partials, should update it 112 | primary fooa 113 | primary fooab 114 | primary fooabc 115 | 116 | # Should have replaced the old one 117 | settle 118 | check_nr_clips 1 119 | [[ "$(< "$l_out")" == "[1] fooabc" ]] 120 | 121 | primary fooab 122 | primary fooa 123 | primary foo 124 | 125 | # Should have replaced the old one 126 | settle 127 | check_nr_clips 1 128 | [[ "$(< "$l_out")" == "[1] foo" ]] 129 | 130 | # Put some more content on the clipboard for testing 131 | primary bar 132 | primary baz 133 | settle 134 | 135 | check_nr_clips 3 136 | 137 | # Nothing gets deleted, but we recognise the right clips 138 | [[ $(clipdel a) == $'bar\nbaz' ]] 139 | check_nr_clips 3 140 | 141 | # Likewise but inversion 142 | [[ $(clipdel -v a) == foo ]] 143 | check_nr_clips 3 144 | 145 | # Real delete with inversion 146 | [[ $(clipdel -dv a) == foo ]] 147 | check_nr_clips 2 148 | 149 | # Test literal match 150 | primary '*foo' 151 | settle 152 | check_nr_clips 3 153 | 154 | [[ $(clipdel -dF '*') == '*foo' ]] 155 | check_nr_clips 2 156 | 157 | # Test -n 158 | primary latest 159 | settle 160 | check_nr_clips 3 161 | 162 | # Should delete the newest entry 163 | [[ $(clipdel -dn 1) == 'latest' ]] 164 | check_nr_clips 2 165 | 166 | # Test -N 167 | primary wibble 168 | primary wobble 169 | settle 170 | check_nr_clips 4 171 | 172 | # Should delete everything other than the oldest 2 entries 173 | [[ $(clipdel -dvN 2) == $'wibble\nwobble' ]] 174 | check_nr_clips 2 175 | 176 | # Check selecting starts serving 177 | xsel -pc 178 | 179 | SELECT='[1] bar' clipmenu 180 | settle 181 | [[ "$(xsel -po)" == bar ]] 182 | 183 | SELECT='[2] baz' clipmenu 184 | settle 185 | [[ "$(xsel -po)" == baz ]] 186 | 187 | # Disable populates no new clips 188 | clipctl disable 189 | [[ "$(clipctl status)" == disabled ]] 190 | primary wibble 191 | primary wobble 192 | settle 193 | check_nr_clips 2 194 | 195 | # Enable enables again 196 | clipctl enable 197 | [[ "$(clipctl status)" == enabled ]] 198 | primary wibble 199 | primary wobble 200 | settle 201 | check_nr_clips 4 202 | 203 | # Toggle works both ways 204 | clipctl toggle 205 | [[ "$(clipctl status)" == disabled ]] 206 | clipctl toggle 207 | [[ "$(clipctl status)" == enabled ]] 208 | 209 | # Test INCR support 210 | set +x 211 | printf '%.0sa' {1..9999999} | xsel -p 212 | set -x 213 | long_settle # This is a big one, give it some time 214 | len_before=$(xsel -po | wc -c) 215 | check_nr_clips 5 216 | SELECT='[5] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' \ 217 | clipmenu 218 | long_settle 219 | len_after=$(xsel -po | wc -c) 220 | 221 | # Make sure we got the whole thing 222 | (( len_before == len_after )) 223 | 224 | # Issue #241. Put a large clipboard selection via INCR, then put something else 225 | # small immediately after. 226 | xsel -bc 227 | settle 228 | check_nr_clips 5 229 | 230 | # Large selection (INCR) 231 | printf '%.0sa' {1..4001} | xsel -b 232 | long_settle 233 | 234 | check_nr_clips 6 235 | 236 | # After large selection is stored, put something small that's a possible # 237 | # partial (non-INCR) 238 | printf a | xsel -b 239 | settle 240 | 241 | check_nr_clips 6 242 | 243 | if (( _UNSHARED )); then 244 | umount -l /tmp 245 | fi 246 | --------------------------------------------------------------------------------