Coverage Report

Created: 2025-07-11 06:20

/src/tmux/hyperlinks.c
Line
Count
Source (jump to first uncovered line)
1
/* $OpenBSD$ */
2
3
/*
4
 * Copyright (c) 2021 Will <author@will.party>
5
 * Copyright (c) 2022 Jeff Chiang <pobomp@gmail.com>
6
 *
7
 * Permission to use, copy, modify, and distribute this software for any
8
 * purpose with or without fee is hereby granted, provided that the above
9
 * copyright notice and this permission notice appear in all copies.
10
 *
11
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
16
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
17
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
 */
19
20
#include <sys/types.h>
21
22
#include <stdlib.h>
23
#include <string.h>
24
25
#include "tmux.h"
26
27
/*
28
 * OSC 8 hyperlinks, described at:
29
 *
30
 *     https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
31
 *
32
 * Each hyperlink and ID combination is assigned a number ("inner" in this
33
 * file) which is stored in an extended grid cell and maps into a tree here.
34
 *
35
 * Each URI has one inner number and one external ID (which tmux uses to send
36
 * the hyperlink to the terminal) and one internal ID (which is received from
37
 * the sending application inside tmux).
38
 *
39
 * Anonymous hyperlinks are each unique and are not reused even if they have
40
 * the same URI (terminals will not want to tie them together).
41
 */
42
43
6.20k
#define MAX_HYPERLINKS 5000
44
45
static long long hyperlinks_next_external_id = 1;
46
static u_int global_hyperlinks_count;
47
48
struct hyperlinks_uri {
49
  struct hyperlinks *tree;
50
51
  u_int      inner;
52
  const char    *internal_id;
53
  const char    *external_id;
54
  const char    *uri;
55
56
  TAILQ_ENTRY(hyperlinks_uri) list_entry;
57
  RB_ENTRY(hyperlinks_uri)    by_inner_entry;
58
  RB_ENTRY(hyperlinks_uri)    by_uri_entry; /* by internal ID and URI */
59
};
60
RB_HEAD(hyperlinks_by_uri_tree, hyperlinks_uri);
61
RB_HEAD(hyperlinks_by_inner_tree, hyperlinks_uri);
62
63
TAILQ_HEAD(hyperlinks_list, hyperlinks_uri);
64
static struct hyperlinks_list global_hyperlinks =
65
    TAILQ_HEAD_INITIALIZER(global_hyperlinks);
66
67
struct hyperlinks {
68
  u_int       next_inner;
69
  struct hyperlinks_by_inner_tree by_inner;
70
  struct hyperlinks_by_uri_tree by_uri;
71
  u_int       references;
72
};
73
74
static int
75
hyperlinks_by_uri_cmp(struct hyperlinks_uri *left, struct hyperlinks_uri *right)
76
34.2k
{
77
34.2k
  int r;
78
79
34.2k
  if (*left->internal_id == '\0' || *right->internal_id == '\0') {
80
    /*
81
     * If both URIs are anonymous, use the inner for comparison so
82
     * that they do not match even if the URI is the same - each
83
     * anonymous URI should be unique.
84
     */
85
22.6k
    if (*left->internal_id != '\0')
86
3.10k
      return (-1);
87
19.4k
    if (*right->internal_id != '\0')
88
382
      return (1);
89
19.1k
    return (left->inner - right->inner);
90
19.4k
  }
91
92
11.6k
  r = strcmp(left->internal_id, right->internal_id);
93
11.6k
  if (r != 0)
94
9.39k
    return (r);
95
2.27k
  return (strcmp(left->uri, right->uri));
96
11.6k
}
97
RB_PROTOTYPE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
98
    hyperlinks_by_uri_cmp);
99
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
100
    hyperlinks_by_uri_cmp);
101
102
static int
103
hyperlinks_by_inner_cmp(struct hyperlinks_uri *left,
104
    struct hyperlinks_uri *right)
105
28.0k
{
106
28.0k
  return (left->inner - right->inner);
107
28.0k
}
108
RB_PROTOTYPE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
109
    hyperlinks_by_inner_cmp);
110
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
111
    hyperlinks_by_inner_cmp);
112
113
/* Remove a hyperlink. */
114
static void
115
hyperlinks_remove(struct hyperlinks_uri *hlu)
116
6.20k
{
117
6.20k
  struct hyperlinks *hl = hlu->tree;
118
119
6.20k
  TAILQ_REMOVE(&global_hyperlinks, hlu, list_entry);
120
6.20k
  global_hyperlinks_count--;
121
122
6.20k
  RB_REMOVE(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
123
6.20k
  RB_REMOVE(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
124
125
6.20k
  free((void *)hlu->internal_id);
126
6.20k
  free((void *)hlu->external_id);
127
6.20k
  free((void *)hlu->uri);
128
6.20k
  free(hlu);
129
6.20k
}
130
131
/* Store a new hyperlink or return if it already exists. */
132
u_int
133
hyperlinks_put(struct hyperlinks *hl, const char *uri_in,
134
    const char *internal_id_in)
135
6.32k
{
136
6.32k
  struct hyperlinks_uri  find, *hlu;
137
6.32k
  char      *uri, *internal_id, *external_id;
138
139
  /*
140
   * Anonymous URI are stored with an empty internal ID and the tree
141
   * comparator will make sure they never match each other (so each
142
   * anonymous URI is unique).
143
   */
144
6.32k
  if (internal_id_in == NULL)
145
3.85k
    internal_id_in = "";
146
147
6.32k
  utf8_stravis(&uri, uri_in, VIS_OCTAL|VIS_CSTYLE);
148
6.32k
  utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE);
149
150
6.32k
  if (*internal_id_in != '\0') {
151
2.47k
    find.uri = uri;
152
2.47k
    find.internal_id = internal_id;
153
154
2.47k
    hlu = RB_FIND(hyperlinks_by_uri_tree, &hl->by_uri, &find);
155
2.47k
    if (hlu != NULL) {
156
129
      free (uri);
157
129
      free (internal_id);
158
129
      return (hlu->inner);
159
129
    }
160
2.47k
  }
161
6.20k
  xasprintf(&external_id, "tmux%llX", hyperlinks_next_external_id++);
162
163
6.20k
  hlu = xcalloc(1, sizeof *hlu);
164
6.20k
  hlu->inner = hl->next_inner++;
165
6.20k
  hlu->internal_id = internal_id;
166
6.20k
  hlu->external_id = external_id;
167
6.20k
  hlu->uri = uri;
168
6.20k
  hlu->tree = hl;
169
6.20k
  RB_INSERT(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
170
6.20k
  RB_INSERT(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
171
172
6.20k
  TAILQ_INSERT_TAIL(&global_hyperlinks, hlu, list_entry);
173
6.20k
  if (++global_hyperlinks_count == MAX_HYPERLINKS)
174
0
    hyperlinks_remove(TAILQ_FIRST(&global_hyperlinks));
175
176
6.20k
  return (hlu->inner);
177
6.32k
}
178
179
/* Get hyperlink by inner number. */
180
int
181
hyperlinks_get(struct hyperlinks *hl, u_int inner, const char **uri_out,
182
    const char **internal_id_out, const char **external_id_out)
183
0
{
184
0
  struct hyperlinks_uri find, *hlu;
185
186
0
  find.inner = inner;
187
188
0
  hlu = RB_FIND(hyperlinks_by_inner_tree, &hl->by_inner, &find);
189
0
  if (hlu == NULL)
190
0
    return (0);
191
0
  if (internal_id_out != NULL)
192
0
    *internal_id_out = hlu->internal_id;
193
0
  if (external_id_out != NULL)
194
0
    *external_id_out = hlu->external_id;
195
0
  *uri_out = hlu->uri;
196
0
  return (1);
197
0
}
198
199
/* Initialize hyperlink set. */
200
struct hyperlinks *
201
hyperlinks_init(void)
202
22.4k
{
203
22.4k
  struct hyperlinks *hl;
204
205
22.4k
  hl = xcalloc(1, sizeof *hl);
206
22.4k
  hl->next_inner = 1;
207
22.4k
  RB_INIT(&hl->by_uri);
208
22.4k
  RB_INIT(&hl->by_inner);
209
22.4k
  hl->references = 1;
210
22.4k
  return (hl);
211
22.4k
}
212
213
/* Copy hyperlink set. */
214
struct hyperlinks *
215
hyperlinks_copy(struct hyperlinks *hl)
216
0
{
217
0
  hl->references++;
218
0
  return (hl);
219
0
}
220
221
/* Free all hyperlinks but not the set itself. */
222
void
223
hyperlinks_reset(struct hyperlinks *hl)
224
22.4k
{
225
22.4k
  struct hyperlinks_uri *hlu, *hlu1;
226
227
22.4k
  RB_FOREACH_SAFE(hlu, hyperlinks_by_inner_tree, &hl->by_inner, hlu1)
228
6.20k
    hyperlinks_remove(hlu);
229
22.4k
}
230
231
/* Free hyperlink set. */
232
void
233
hyperlinks_free(struct hyperlinks *hl)
234
22.4k
{
235
22.4k
  if (--hl->references == 0) {
236
22.4k
    hyperlinks_reset(hl);
237
22.4k
    free(hl);
238
22.4k
  }
239
22.4k
}