Importar clipmenu (rama develop) desde https://github.com/cdown/clipmenu

This commit is contained in:
Alejandro Guerrero 2026-09-25 10:55:49 +02:00
commit eaf587b1f9
32 changed files with 5760 additions and 0 deletions

847
clipmenu/src/store.c Normal file
View file

@ -0,0 +1,847 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "store.h"
/**
* TERMINOLOGY
*
* - Clip store: A pairing of a file containing snips and a content directory
* - Clip store entry: A single pair of snip and content entry
* - Snip file: A file which maps lines to content entries using hashes
* - Snip: A (hash, line) pair in the clip store, representing a content entry
* - Content entry: The full data stored from the clipboard
* - Content directory: The directory in which the content entries are stored
*
* KEY FUNCTIONS:
*
* - cs_add - add a clip store entry
* - cs_remove - remove a clip store entry by callback
* - cs_trim - trim to the newest/oldest N entries
* - cs_snip_iter - iterate over snip hashes and lines
* - cs_content_get - get the content for a snip hash
*
* CLIP STORE DESIGN
*
* Our primary focus is on achieving high efficiency in appending new snips,
* iterating over the entire list of snips, and replacing the final snip in the
* snip file. For this reason complexity is avoided in processing deletions,
* since deletions are usually rare. This avoids us having to implement (for
* example) tombstones, which would slow things down and complicate iteration.
*
* CONTENT DIRECTORY DESIGN
*
* The content directory is extremely simple: it contains files with the same
* name as the snip hash. This allows quickly going from the one line summary
* in the snip to the full contents.
*
* SYNCHRONISATION
*
* The clip store's size may be increased or decreased by another program using
* the library, so before each operation we must take a lock and check if the
* header was updated. If it was, we update the size of the mmapped area to
* suit. The lock is implemented using flock() on cs->snip_fd, see cs_ref(),
* cs_ref_no_update(), and cs_unref().
*/
/**
* Calculate the needed file size in bytes for @nr_snips snips, adding the
* header.
*
* @nr_snips: The number of snips to calculate for
*/
static size_t _must_use_ cs_file_size(size_t nr_snips) {
return (nr_snips + 1) * CS_SNIP_SIZE;
}
/**
* Validate the consistency of the clip store's header information.
*
* @cs: The clip store to operate on
* @file_size: The current size of the file
*/
static int _must_use_ _nonnull_ cs_header_validate(const struct clip_store *cs,
size_t file_size) {
if (cs->header->nr_snips > cs->header->nr_snips_alloc ||
(cs->header->nr_snips_alloc + 1) * CS_SNIP_SIZE != file_size) {
return -EINVAL;
}
return 0;
}
static int _must_use_ _nonnull_ cs_remap(struct clip_store *cs, size_t old_size,
size_t new_size) {
struct cs_header *new_header = mmap(NULL, new_size, PROT_READ | PROT_WRITE,
MAP_SHARED, cs->snip_fd, 0);
if (new_header == MAP_FAILED) {
return negative_errno();
}
if (munmap(cs->header, old_size) < 0) {
int ret = negative_errno();
munmap(new_header, new_size);
return ret;
}
cs->header = new_header;
cs->snips = (struct cs_snip *)(cs->header + 1);
cs->mapped_file_size = new_size;
return 0;
}
/**
* Decrease the reference count for the clip store lock, unrefing it if
* the refcount reaches zero.
*
* @cs: The clip store to operate on
*/
void cs_unref(struct clip_store *cs) {
expect(cs->refcount > 0);
cs->refcount--;
if (cs->refcount == 0) {
expect(flock(cs->snip_fd, LOCK_UN) == 0);
}
}
/**
* Increase the reference count for the clip store lock.
*
* @cs: The clip store to operate on
*/
static struct ref_guard _must_use_ _nonnull_
cs_ref_no_update(struct clip_store *cs) {
struct ref_guard guard = {.status = 0, .unref = cs_unref, .cs = cs};
if (cs->refcount == 0) {
expect(flock(cs->snip_fd, LOCK_EX) == 0);
}
static_assert(sizeof(cs->refcount) == sizeof(size_t),
"refcount type wrong");
expect(cs->refcount < SIZE_MAX);
cs->refcount++;
return guard;
}
/**
* Increase the reference count for the clip store lock, and remap as needed if
* the header values have changed.
*
* Even if the guard status indicates an error, you must still call cs_unref().
*
* @cs: The clip store to operate on
*/
struct ref_guard cs_ref(struct clip_store *cs) {
struct ref_guard guard = cs_ref_no_update(cs);
if (cs->refcount > 1) {
// We're an inner reference, so any necessary remapping has already
// been performed.
return guard;
}
if (cs->local_nr_snips != cs->header->nr_snips ||
cs->local_nr_snips_alloc != cs->header->nr_snips_alloc) {
struct stat st;
if (fstat(cs->snip_fd, &st) < 0) {
guard.status = negative_errno();
return guard;
}
int ret = cs_header_validate(cs, (size_t)st.st_size);
if (ret < 0) {
guard.status = ret;
return guard;
}
// If we shrank, no need to remap, since we'll just use the new bounds.
if (cs->local_nr_snips_alloc < cs->header->nr_snips_alloc) {
int remap_ret = cs_remap(cs, cs->mapped_file_size,
cs_file_size(cs->header->nr_snips_alloc));
if (remap_ret < 0) {
guard.status = remap_ret;
return guard;
}
}
cs->local_nr_snips = cs->header->nr_snips;
cs->local_nr_snips_alloc = cs->header->nr_snips_alloc;
}
return guard;
}
/**
* _drop_() function for when a `ref_guard` structure goes out of scope.
*
* @guard: The guard lock
*/
void drop_cs_unref(struct ref_guard *guard) {
guard->status = -EBADF;
guard->unref(guard->cs);
}
/**
* Destroy the clip store, releasing all of its resources.
*
* @cs: The clip store to operate on
*/
int cs_destroy(struct clip_store *cs) {
cs->ready = false;
if (munmap(cs->header, cs->mapped_file_size)) {
return negative_errno();
}
return 0;
}
/**
* _drop_() function for when a `clip_store` goes out of scope.
*
* @guard: The guard lock
*/
void drop_cs_destroy(struct clip_store *cs) { expect(cs_destroy(cs) == 0); }
/**
* Initialise a `struct clip_store` with snip_fd open to a file for snip
* storage and content_fd open to a directory for content entry storage.
*
* The snip file is extended and the header snip is written if the file size is
* zero. The file is mapped into memory until cs_destroy() is called.
*
* @cs: The clip store to initialise
* @snip_fd: Open file descriptor for the snip file
* @content_dir_fd: Open file descriptor for the content directory
*/
int cs_init(struct clip_store *cs, int snip_fd, int content_dir_fd) {
cs->ready = false;
cs->snip_fd = snip_fd;
cs->content_dir_fd = content_dir_fd;
cs->refcount = 0;
_drop_(cs_unref) struct ref_guard guard = cs_ref_no_update(cs);
struct stat st;
if (fstat(snip_fd, &st) < 0) {
return negative_errno();
}
if (st.st_size % CS_SNIP_SIZE != 0) {
return -EINVAL;
}
size_t file_size = (size_t)st.st_size;
if (file_size == 0) {
file_size = CS_SNIP_SIZE;
if (ftruncate(snip_fd, (off_t)file_size) < 0) {
return negative_errno();
}
}
cs->header =
mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, snip_fd, 0);
if (cs->header == MAP_FAILED) {
return negative_errno();
}
int ret = cs_header_validate(cs, file_size);
if (ret < 0) {
munmap(cs->header, file_size);
return ret;
}
cs->snips = (struct cs_snip *)(cs->header + 1);
cs->local_nr_snips = cs->header->nr_snips;
cs->local_nr_snips_alloc = cs->header->nr_snips_alloc;
cs->mapped_file_size = file_size;
cs->ready = true;
(void)guard; // Old clang will complain guard is unused, despite cleanup
return 0;
}
/**
* Round up a number to the nearest multiple of a specified step.
*
* @n: The number to round up
* @step: The step size to round up to
*/
static size_t round_up(size_t n, size_t step) {
return ((n + step - 1) / step) * step;
}
/**
* Adjust the size of the mmapped file used by the clip store to the specified
* new size. It first attempts to resize the file using ftruncate(). If this
* operation is successful, it then remaps the memory mapping to reflect the
* new size of the file.
*
* WARNING: cs->snips and cs->header may move after cs_file_resize(), so
* copied pointers from before invocation must not be used after calling this.
*
* @cs: The clip store to operate on
* @new_nr_snips: The new number of snips in the snip file
*/
static int _must_use_ _nonnull_ cs_file_resize(struct clip_store *cs,
size_t new_nr_snips) {
bool grow = new_nr_snips >= cs->header->nr_snips;
if (grow && new_nr_snips <= cs->header->nr_snips_alloc) {
cs->header->nr_snips = new_nr_snips;
return 0;
}
/* If this is a shrink, do it exactly: someone may be deleting sensitive
* snips and so it's best to remove them immediately. Otherwise, batch
* allocate. */
size_t new_nr_snips_alloc =
grow ? round_up(new_nr_snips, CS_SNIP_ALLOC_BATCH) : new_nr_snips;
size_t new_size = cs_file_size(new_nr_snips_alloc);
if (ftruncate(cs->snip_fd, (off_t)new_size) < 0) {
return negative_errno();
}
if (grow) {
size_t old_file_size = cs_file_size(cs->header->nr_snips_alloc);
int ret = cs_remap(cs, cs->mapped_file_size, new_size);
if (ret < 0) {
expect(ftruncate(cs->snip_fd, (off_t)old_file_size) == 0);
return ret;
}
}
cs->header->nr_snips = cs->local_nr_snips = new_nr_snips;
cs->header->nr_snips_alloc = cs->local_nr_snips_alloc = new_nr_snips_alloc;
return 0;
}
/**
* Update a clip store snip to contain the specified hash and line content.
*
* @snip: Pointer to the snip to modify
* @hash: The new hash value for the snip
* @line: The new line content for the snip
* @nr_lines: The number of lines in the line content
*/
static void _nonnull_ cs_snip_update(struct cs_snip *snip, uint64_t hash,
const char *line, uint64_t nr_lines) {
snip->hash = hash;
snip->doomed = false;
snip->nr_lines = nr_lines;
strncpy(snip->line, line, CS_SNIP_LINE_SIZE - 1);
snip->line[CS_SNIP_LINE_SIZE - 1] = '\0';
}
/**
* Extracts the first non-empty line from a given text buffer and copies it to
* the output buffer. Returns the total number of lines. A final line with no
* newline is considered a line for accounting purposes.
*
* @text: The input text buffer
* @out: The output buffer. Must be at least CS_SNIP_LINE_SIZE bytes
*/
size_t first_line(const char *text, char *out) {
bool found = false;
size_t nr_lines = 0;
const char *cur = text;
out[0] = '\0';
for (; *cur; cur++) {
nr_lines += (*cur == '\n');
if (!found && *cur != '\n') {
found = true;
snprintf(out, CS_SNIP_LINE_SIZE, "%.*s", (int)strcspn(cur, "\n"),
cur);
}
}
return nr_lines + (found && *--cur != '\n');
}
/**
* Add a new snip consisting of a hash value and a line of text to the end of
* the clip store. The snip file size is grown as necessary to accommodate the
* new snip.
*
* @cs: The clip store to operate on
* @hash: The hash value of the snip to add
* @line: The line content of the snip to add
* @nr_lines: The number of lines in the line content
*/
static int _must_use_ _nonnull_ cs_snip_add(struct clip_store *cs,
uint64_t hash, const char *line,
uint64_t nr_lines) {
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
int ret = cs_file_resize(cs, cs->header->nr_snips + 1);
if (ret < 0) {
return ret;
}
cs_snip_update(cs->snips + cs->header->nr_snips - 1, hash, line, nr_lines);
return 0;
}
/**
* Add content to the content directory using the hash as the filename.
*
* @cs: The clip store to operate on
* @hash: The hash of the content to add
* @content: The content to add to the file
* @dupe_policy: If set to CS_DUPE_KEEP_LAST, will return with -EEXIST when
* trying to insert duplicate entry.
*/
static int _must_use_ _nonnull_
cs_content_add(struct clip_store *cs, uint64_t hash, const char *content,
enum cs_dupe_policy dupe_policy) {
bool dupe = false;
size_t content_len = strlen(content);
char dir_path[CS_HASH_STR_MAX];
snprintf(dir_path, sizeof(dir_path), PRI_HASH, hash);
int ret = mkdirat(cs->content_dir_fd, dir_path, 0700);
if (ret < 0) {
if (errno != EEXIST || dupe_policy == CS_DUPE_KEEP_LAST) {
return negative_errno();
}
dupe = true;
}
char base_file_path[PATH_MAX];
snprintf(base_file_path, sizeof(base_file_path), "%s/1", dir_path);
if (dupe) {
// This clip already exists, just create a link for refcounting
struct stat st;
if (fstatat(cs->content_dir_fd, base_file_path, &st, 0) < 0) {
return negative_errno();
}
if ((size_t)st.st_size != content_len) {
// Extremely unlikely with FNV-1a 64-bit outside of artificial
// scenarios, but never hurts to be careful...
return -EFAULT;
}
size_t link_num = (size_t)st.st_nlink + 1;
char linkpath[PATH_MAX];
snprintf(linkpath, sizeof(linkpath), "%s/%zu", dir_path, link_num);
if (linkat(cs->content_dir_fd, base_file_path, cs->content_dir_fd,
linkpath, 0) < 0) {
return negative_errno();
}
return 0;
}
// This is a new clip
_drop_(close) int fd = openat(cs->content_dir_fd, base_file_path,
O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0) {
return negative_errno();
}
const char *cur = content;
size_t remaining = content_len;
while (remaining > 0) {
ssize_t written = write(fd, cur, remaining);
if (written < 0) {
return negative_errno();
}
remaining -= (size_t)written;
cur += written;
}
return 0;
}
/**
* Clean up the backing mapped data and file descriptor for a `struct
* cs_content` object.
*
* @content: The content to unmap
*/
static int cs_content_unmap(struct cs_content *content) {
if (content && content->data) {
close(content->fd);
if (munmap(content->data, (size_t)content->size)) {
return negative_errno();
}
}
return 0;
}
/**
* _drop_ function for cs_content_unmap()
*
* @content: The content to unmap
*/
void drop_cs_content_unmap(struct cs_content *content) {
int ret = cs_content_unmap(content);
expect(ret == 0);
}
/**
* Retrieve the content associated with a given hash from the content directory
* and map it into memory.
*
* @cs: The clip store to operate on
* @hash: The hash of the content to retrieve
* @content: A pointer to a `struct cs_content` to populate. The caller must
* call cs_content_unmap() when done to free it
*/
int cs_content_get(struct clip_store *cs, uint64_t hash,
struct cs_content *content) {
memset(content, '\0', sizeof(struct cs_content));
char filename[PATH_MAX];
snprintf(filename, sizeof(filename), PRI_HASH "/1", hash);
_drop_(close) int fd = openat(cs->content_dir_fd, filename, O_RDONLY);
if (fd < 0) {
return negative_errno();
}
struct stat st;
if (fstat(fd, &st) < 0) {
return negative_errno();
}
char *data = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) {
return negative_errno();
}
content->data = data;
content->fd = fd;
content->size = st.st_size;
fd = -1; // Disarm the _drop_, this will be handled by cs_content_unmap()
return 0;
}
/**
* Move the entry with the specified hash to the newest slot.
*
* @cs: The clip store to operate on
* @hash: The hash of the entry to move
*/
int cs_make_newest(struct clip_store *cs, uint64_t hash) {
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
for (int i = 0; i < (int)cs->local_nr_snips; ++i) {
if (cs->snips[i].hash == hash) {
struct cs_snip tmp = cs->snips[i];
memmove(cs->snips + i, cs->snips + i + 1,
(cs->local_nr_snips - (i + 1)) * sizeof(*cs->snips));
cs->snips[cs->local_nr_snips - 1] = tmp;
return 0;
}
}
return -ENOENT;
}
static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs,
uint64_t hash);
/**
* Add a new content entry to the clip store and content directory.
*
* @cs: The clip store to operate on
* @content: The content to add
* @out_hash: Output for the generated hash, or NULL
* @dupe_policy: Policy to use for duplicate entries
*/
int cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash,
enum cs_dupe_policy dupe_policy) {
uint64_t hash = fnv1a_64_hash(content);
char line[CS_SNIP_LINE_SIZE];
size_t nr_lines = first_line(content, line);
if (out_hash) {
*out_hash = hash;
}
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
int ret = cs_content_add(cs, hash, content, dupe_policy);
if (ret == -EEXIST && dupe_policy == CS_DUPE_KEEP_LAST) {
return cs_make_newest(cs, hash);
}
if (ret) {
return ret;
}
ret = cs_snip_add(cs, hash, line, nr_lines);
if (ret) {
int rm_ret = cs_content_remove(cs, hash);
if (rm_ret) {
warn("Failed to roll back leaked content for hash " PRI_HASH
" after cs_snip_add failed (%s): %s\n",
hash, strerror(-ret), strerror(-rm_ret));
return rm_ret;
}
}
return ret;
}
/**
* Iterate over the snips in the clip store. The function should be initially
* called with *snip set to NULL, which will set *snip to point to the newest
* snip (excluding the header). On subsequent calls, *snip is updated to point
* to the next snip in the snip file. The iteration stops when there are no
* more snips to process, indicated by the function returning false.
*
* @guard: The guard lock
* @snip: Pointer to a pointer to the current snip being iterated over
* @direction: Whether to iterate from the oldest to newest or vice versa
*/
bool cs_snip_iter(struct ref_guard *guard, enum cs_iter_direction direction,
struct cs_snip **snip) {
if (guard->status < 0 || guard->cs->header->nr_snips == 0) {
return false;
}
struct cs_snip *oldest = guard->cs->snips;
struct cs_snip *newest = oldest + (guard->cs->header->nr_snips - 1);
const struct cs_snip *stop =
direction == CS_ITER_NEWEST_FIRST ? oldest : newest;
if (!*snip) {
*snip = direction == CS_ITER_NEWEST_FIRST ? newest : oldest;
return true;
} else if (*snip != stop) {
*snip = *snip + (direction == CS_ITER_NEWEST_FIRST ? -1 : 1);
return true;
}
return false;
}
/**
* Remove content from the content directory using the hash as the filename.
*
* @cs: The clip store to operate on
* @hash: The hash of the content to remove
*/
static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs,
uint64_t hash) {
char hash_dir_name[CS_HASH_STR_MAX];
snprintf(hash_dir_name, sizeof(hash_dir_name), PRI_HASH, hash);
_drop_(close) int hash_dir_fd =
openat(cs->content_dir_fd, hash_dir_name, O_RDONLY);
if (hash_dir_fd < 0) {
return negative_errno();
}
struct stat st;
if (fstatat(hash_dir_fd, "1", &st, 0) < 0) {
return negative_errno();
}
char nlink_path[PATH_MAX];
snprintf(nlink_path, sizeof(nlink_path), "%u", (unsigned)st.st_nlink);
if (unlinkat(hash_dir_fd, nlink_path, 0) < 0) {
return negative_errno();
}
if (st.st_nlink == 1 &&
unlinkat(cs->content_dir_fd, hash_dir_name, AT_REMOVEDIR) < 0) {
return negative_errno();
}
return 0;
}
/**
* Compacts the clip store by removing doomed snips, finalising their removal
* after being marked in cs_remove().
*
* @guard: The guard lock
*/
static size_t _nonnull_ cs_snip_remove_doomed(struct ref_guard *guard) {
size_t nr_doomed = 0;
struct cs_snip *snip = NULL;
while (cs_snip_iter(guard, CS_ITER_OLDEST_FIRST, &snip)) {
if (snip->doomed) {
nr_doomed++;
} else if (nr_doomed > 0) {
*(snip - nr_doomed) = *snip;
}
}
return nr_doomed;
}
/**
* Iterate over the specified number of snips in the clip store from newest
* to oldest and remove those for which the predicate function returns
* CS_ACTION_REMOVE (see `enum cs_remove_action` above).
*
* @cs: The clip store to operate on
* @should_remove: Function pointer to the predicate used to decide removal
* @private: Pointer to user-defined data passed to the predicate function
* @direction: Whether to iterate from the oldest to newest or vice versa
*/
int cs_remove(struct clip_store *cs, enum cs_iter_direction direction,
enum cs_remove_action (*should_remove)(uint64_t, const char *,
void *),
void *private) {
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
bool found = false;
struct cs_snip *snip = NULL;
while (cs_snip_iter(&guard, direction, &snip)) {
enum cs_remove_action action =
should_remove(snip->hash, snip->line, private);
if (action & CS_ACTION_REMOVE) {
found = true;
int ret = cs_content_remove(cs, snip->hash);
if (ret < 0) {
return ret;
}
snip->doomed = true;
}
if (action & CS_ACTION_STOP) {
break;
}
}
if (!found) {
return 0;
}
size_t nr_doomed = cs_snip_remove_doomed(&guard);
int ret = cs_file_resize(cs, cs->header->nr_snips - nr_doomed);
if (ret < 0) {
return ret;
}
return 0;
}
/**
* Callback function for deciding which snips to remove during trim operation.
*
* @hash: Unused
* @line: Unused
* @private: The count of remaining entries to trim
*/
static enum cs_remove_action _must_use_ _nonnull_
trim_callback(uint64_t hash, const char *line, void *private) {
(void)hash;
(void)line;
size_t *count = private;
if (*count == 0) {
return CS_ACTION_REMOVE;
}
(*count)--;
return CS_ACTION_KEEP;
}
/**
* Trim the clip store to only retain the specified number of snips.
*
* @cs: The clip store to operate on
* @direction: Whether to remove the N newest or N oldest
* @nr_keep: The number of newest snips to retain
*/
int cs_trim(struct clip_store *cs, enum cs_iter_direction direction,
size_t nr_keep) {
if (nr_keep >= cs->header->nr_snips) {
return 0; // No action needed if we're keeping everything or more
}
int ret = cs_remove(cs, direction, trim_callback, &nr_keep);
if (ret < 0) {
return ret;
}
return 0;
}
/**
* Replace the content and snip for an entry in the clip store, identified by
* its age.
*
* @cs: The clip store to operate on
* @age: The age of the snip to replace, with 0 being the newest
* @direction: Whether to iterate from the oldest to newest or vice versa
* @content: The content to replace this entry with
* @out_hash: Output for the generated hash, or NULL
*/
int cs_replace(struct clip_store *cs, enum cs_iter_direction direction,
size_t age, const char *content, uint64_t *out_hash) {
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
if (age >= cs->header->nr_snips) {
return -ERANGE;
}
size_t idx = direction == CS_ITER_NEWEST_FIRST
? cs->header->nr_snips - age - 1
: age;
struct cs_snip *snip = cs->snips + idx;
char line[CS_SNIP_LINE_SIZE];
size_t nr_lines = first_line(content, line);
uint64_t old_hash = snip->hash;
uint64_t hash = fnv1a_64_hash(content);
int ret = cs_content_add(cs, hash, content, CS_DUPE_KEEP_ALL);
if (ret) {
return ret;
}
ret = cs_content_remove(cs, old_hash);
if (ret) {
return ret;
}
cs_snip_update(snip, hash, line, nr_lines);
if (out_hash) {
*out_hash = hash;
}
return 0;
}
/**
* Get the current number of entries in the clip store.
*
* @cs: The clip store to operate on
* @out_len: Output for the length of the clip store
*/
int cs_len(struct clip_store *cs, size_t *out_len) {
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
if (guard.status < 0) {
return guard.status;
}
*out_len = cs->header->nr_snips;
return 0;
}