root/usr.bin/tmux/paste.c
/* $OpenBSD: paste.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */

/*
 * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <vis.h>

#include "tmux.h"

/*
 * Set of paste buffers. Note that paste buffer data is not necessarily a C
 * string!
 */

static u_int    paste_next_index;
static u_int    paste_next_order;
static u_int    paste_num_automatic;
static RB_HEAD(paste_name_tree, paste_buffer) paste_by_name;
static RB_HEAD(paste_time_tree, paste_buffer) paste_by_time;

static int      paste_cmp_names(const struct paste_buffer *,
                    const struct paste_buffer *);
RB_GENERATE_STATIC(paste_name_tree, paste_buffer, name_entry, paste_cmp_names);

static int      paste_cmp_times(const struct paste_buffer *,
                    const struct paste_buffer *);
RB_GENERATE_STATIC(paste_time_tree, paste_buffer, time_entry, paste_cmp_times);

static void
paste_fire_event(const char *name, const char *pbname)
{
        struct event_payload    *ep;

        ep = event_payload_create();
        event_payload_set_string(ep, "paste_buffer", "%s", pbname);
        events_fire(name, ep);
}

static int
paste_cmp_names(const struct paste_buffer *a, const struct paste_buffer *b)
{
        return (strcmp(a->name, b->name));
}

static int
paste_cmp_times(const struct paste_buffer *a, const struct paste_buffer *b)
{
        if (a->order > b->order)
                return (-1);
        if (a->order < b->order)
                return (1);
        return (0);
}

/* Get paste buffer name. */
const char *
paste_buffer_name(struct paste_buffer *pb)
{
        return (pb->name);
}

/* Get paste buffer order. */
u_int
paste_buffer_order(struct paste_buffer *pb)
{
        return (pb->order);
}

/* Get paste buffer created. */
time_t
paste_buffer_created(struct paste_buffer *pb)
{
        return (pb->created);
}

/* Get paste buffer data. */
const char *
paste_buffer_data(struct paste_buffer *pb, size_t *size)
{
        if (size != NULL)
                *size = pb->size;
        return (pb->data);
}

/* Walk paste buffers by time. */
struct paste_buffer *
paste_walk(struct paste_buffer *pb)
{
        if (pb == NULL)
                return (RB_MIN(paste_time_tree, &paste_by_time));
        return (RB_NEXT(paste_time_tree, &paste_by_time, pb));
}

int
paste_is_empty(void)
{
        return RB_ROOT(&paste_by_time) == NULL;
}

/* Get the most recent automatic buffer. */
struct paste_buffer *
paste_get_top(char **name)
{
        struct paste_buffer     *pb;

        pb = RB_MIN(paste_time_tree, &paste_by_time);
        while (pb != NULL && !pb->automatic)
                pb = RB_NEXT(paste_time_tree, &paste_by_time, pb);
        if (pb == NULL)
                return (NULL);
        if (name != NULL)
                *name = xstrdup(pb->name);
        return (pb);
}

/* Get a paste buffer by name. */
struct paste_buffer *
paste_get_name(const char *name)
{
        struct paste_buffer     pbfind;

        if (name == NULL || *name == '\0')
                return (NULL);

        pbfind.name = (char *)name;
        return (RB_FIND(paste_name_tree, &paste_by_name, &pbfind));
}

/* Free a paste buffer. */
void
paste_free(struct paste_buffer *pb)
{
        paste_fire_event("paste-buffer-deleted", pb->name);

        RB_REMOVE(paste_name_tree, &paste_by_name, pb);
        RB_REMOVE(paste_time_tree, &paste_by_time, pb);
        if (pb->automatic)
                paste_num_automatic--;

        free(pb->data);
        free(pb->name);
        free(pb);
}

/*
 * Add an automatic buffer, freeing the oldest automatic item if at limit. Note
 * that the caller is responsible for allocating data.
 */
void
paste_add(const char *prefix, char *data, size_t size)
{
        struct paste_buffer     *pb, *pb1;
        u_int                    limit;

        if (prefix == NULL)
                prefix = "buffer";

        if (size == 0) {
                free(data);
                return;
        }

        limit = options_get_number(global_options, "buffer-limit");
        RB_FOREACH_REVERSE_SAFE(pb, paste_time_tree, &paste_by_time, pb1) {
                if (paste_num_automatic < limit)
                        break;
                if (pb->automatic)
                        paste_free(pb);
        }

        pb = xmalloc(sizeof *pb);
        pb->name = NULL;
        do {
                free(pb->name);
                xasprintf(&pb->name, "%s%u", prefix, paste_next_index);
                paste_next_index++;
        } while (paste_get_name(pb->name) != NULL);

        pb->data = data;
        pb->size = size;

        pb->automatic = 1;
        paste_num_automatic++;

        pb->created = time(NULL);

        pb->order = paste_next_order++;
        RB_INSERT(paste_name_tree, &paste_by_name, pb);
        RB_INSERT(paste_time_tree, &paste_by_time, pb);

        paste_fire_event("paste-buffer-changed", pb->name);
}

/* Rename a paste buffer. */
int
paste_rename(const char *oldname, const char *newname, char **cause)
{
        struct paste_buffer     *pb, *pb_new;
        char                    *name;

        if (cause != NULL)
                *cause = NULL;

        if (oldname == NULL || *oldname == '\0') {
                if (cause != NULL)
                        *cause = xstrdup("no buffer");
                return (-1);
        }
        if (newname == NULL || *newname == '\0') {
                if (cause != NULL)
                        *cause = xstrdup("new name is empty");
                return (-1);
        }

        name = clean_name(newname, 0);
        if (name == NULL) {
                if (cause != NULL)
                        xasprintf(cause, "invalid buffer name: %s", newname);
                return (-1);
        }

        pb = paste_get_name(oldname);
        if (pb == NULL) {
                if (cause != NULL)
                        xasprintf(cause, "no buffer %s", oldname);
                free(name);
                return (-1);
        }

        pb_new = paste_get_name(name);
        if (pb_new == pb) {
                free(name);
                return (0);
        }
        if (pb_new != NULL)
                paste_free(pb_new);

        RB_REMOVE(paste_name_tree, &paste_by_name, pb);

        free(pb->name);
        pb->name = name;

        if (pb->automatic)
                paste_num_automatic--;
        pb->automatic = 0;

        RB_INSERT(paste_name_tree, &paste_by_name, pb);

        paste_fire_event("paste-buffer-deleted", oldname);
        paste_fire_event("paste-buffer-changed", pb->name);

        return (0);
}

/*
 * Add or replace an item in the store. Note that the caller is responsible for
 * allocating data.
 */
int
paste_set(char *data, size_t size, const char *name, char **cause)
{
        struct paste_buffer     *pb, *old;
        char                    *newname;

        if (cause != NULL)
                *cause = NULL;

        if (size == 0) {
                free(data);
                return (0);
        }
        if (name == NULL) {
                paste_add(NULL, data, size);
                return (0);
        }

        if (*name == '\0') {
                if (cause != NULL)
                        *cause = xstrdup("empty buffer name");
                return (-1);
        }

        newname = clean_name(name, 0);
        if (newname == NULL) {
                if (cause != NULL)
                        xasprintf(cause, "invalid buffer name: %s", name);
                return (-1);
        }

        pb = xmalloc(sizeof *pb);
        pb->name = newname;

        pb->data = data;
        pb->size = size;

        pb->automatic = 0;
        pb->order = paste_next_order++;

        pb->created = time(NULL);

        if ((old = paste_get_name(pb->name)) != NULL)
                paste_free(old);

        RB_INSERT(paste_name_tree, &paste_by_name, pb);
        RB_INSERT(paste_time_tree, &paste_by_time, pb);

        paste_fire_event("paste-buffer-changed", pb->name);

        return (0);
}

/* Set paste data without otherwise changing it. */
void
paste_replace(struct paste_buffer *pb, char *data, size_t size)
{
        free(pb->data);
        pb->data = data;
        pb->size = size;

        paste_fire_event("paste-buffer-changed", pb->name);
}

/* Convert start of buffer into a nice string. */
char *
paste_make_sample(struct paste_buffer *pb)
{
        char            *buf;
        size_t           len, used;
        const int        flags = VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL;
        const size_t     width = 200;

        len = pb->size;
        if (len > width)
                len = width;
        buf = xreallocarray(NULL, len, 4 + 4);

        used = utf8_strvis(buf, pb->data, len, flags);
        if (pb->size > width || used > width)
                strlcpy(buf + width, "...", 4);
        return (buf);
}