Coverage Report

Created: 2026-10-05 06:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/hyperlinks.c
Line
Count
Source
1
/* $OpenBSD: hyperlinks.c,v 1.6 2026/09/28 10:42:01 nicm Exp $ */
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.90k
#define MAX_HYPERLINKS 5000
44
7.04k
#define MAX_HYPERLINK_URI 1024
45
46
static long long hyperlinks_next_external_id = 1;
47
static u_int global_hyperlinks_count;
48
49
struct hyperlinks_uri {
50
  struct hyperlinks *tree;
51
52
  u_int      inner;
53
  const char    *internal_id;
54
  const char    *external_id;
55
  const char    *uri;
56
57
  TAILQ_ENTRY(hyperlinks_uri) list_entry;
58
  RB_ENTRY(hyperlinks_uri)    by_inner_entry;
59
  RB_ENTRY(hyperlinks_uri)    by_uri_entry; /* by internal ID and URI */
60
};
61
RB_HEAD(hyperlinks_by_uri_tree, hyperlinks_uri);
62
RB_HEAD(hyperlinks_by_inner_tree, hyperlinks_uri);
63
64
TAILQ_HEAD(hyperlinks_list, hyperlinks_uri);
65
static struct hyperlinks_list global_hyperlinks =
66
    TAILQ_HEAD_INITIALIZER(global_hyperlinks);
67
68
struct hyperlinks {
69
  u_int       next_inner;
70
  struct hyperlinks_by_inner_tree by_inner;
71
  struct hyperlinks_by_uri_tree by_uri;
72
  u_int       references;
73
};
74
75
static int
76
hyperlinks_by_uri_cmp(struct hyperlinks_uri *left, struct hyperlinks_uri *right)
77
38.8k
{
78
38.8k
  int r;
79
80
38.8k
  if (*left->internal_id == '\0' || *right->internal_id == '\0') {
81
    /*
82
     * If both URIs are anonymous, use the inner for comparison so
83
     * that they do not match even if the URI is the same - each
84
     * anonymous URI should be unique.
85
     */
86
26.8k
    if (*left->internal_id != '\0')
87
3.36k
      return (-1);
88
23.4k
    if (*right->internal_id != '\0')
89
349
      return (1);
90
23.1k
    if (left->inner < right->inner)
91
0
      return (-1);
92
23.1k
    if (left->inner > right->inner)
93
23.1k
      return (1);
94
0
    return (0);
95
23.1k
  }
96
97
11.9k
  r = strcmp(left->internal_id, right->internal_id);
98
11.9k
  if (r != 0)
99
10.1k
    return (r);
100
1.82k
  return (strcmp(left->uri, right->uri));
101
11.9k
}
102
RB_PROTOTYPE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
103
    hyperlinks_by_uri_cmp);
104
81.2k
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
hyperlinks.c:hyperlinks_by_uri_tree_RB_FIND
Line
Count
Source
104
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
hyperlinks.c:hyperlinks_by_uri_tree_RB_INSERT
Line
Count
Source
104
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
hyperlinks.c:hyperlinks_by_uri_tree_RB_REMOVE
Line
Count
Source
104
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
hyperlinks.c:hyperlinks_by_uri_tree_RB_REMOVE_COLOR
Line
Count
Source
104
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
105
81.2k
    hyperlinks_by_uri_cmp);
106
81.2k
107
81.2k
static int
108
81.2k
hyperlinks_by_inner_cmp(struct hyperlinks_uri *left,
109
81.2k
    struct hyperlinks_uri *right)
110
81.2k
{
111
32.3k
  if (left->inner < right->inner)
112
0
    return (-1);
113
32.3k
  if (left->inner > right->inner)
114
32.3k
    return (1);
115
0
  return (0);
116
32.3k
}
117
RB_PROTOTYPE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
118
    hyperlinks_by_inner_cmp);
119
100k
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
hyperlinks.c:hyperlinks_by_inner_tree_RB_INSERT
Line
Count
Source
119
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
hyperlinks.c:hyperlinks_by_inner_tree_RB_REMOVE
Line
Count
Source
119
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
hyperlinks.c:hyperlinks_by_inner_tree_RB_REMOVE_COLOR
Line
Count
Source
119
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
Unexecuted instantiation: hyperlinks.c:hyperlinks_by_inner_tree_RB_FIND
hyperlinks.c:hyperlinks_by_inner_tree_RB_MINMAX
Line
Count
Source
119
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
120
100k
    hyperlinks_by_inner_cmp);
121
100k
122
100k
/* Remove a hyperlink. */
123
100k
static void
124
100k
hyperlinks_remove(struct hyperlinks_uri *hlu)
125
100k
{
126
6.90k
  struct hyperlinks *hl = hlu->tree;
127
128
6.90k
  TAILQ_REMOVE(&global_hyperlinks, hlu, list_entry);
129
6.90k
  global_hyperlinks_count--;
130
131
6.90k
  RB_REMOVE(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
132
6.90k
  RB_REMOVE(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
133
134
6.90k
  free((void *)hlu->internal_id);
135
6.90k
  free((void *)hlu->external_id);
136
6.90k
  free((void *)hlu->uri);
137
6.90k
  free(hlu);
138
6.90k
}
139
140
/* Store a new hyperlink or return if it already exists. */
141
u_int
142
hyperlinks_put(struct hyperlinks *hl, const char *uri_in,
143
    const char *internal_id_in)
144
7.04k
{
145
7.04k
  struct hyperlinks_uri  find, *hlu;
146
7.04k
  char      *uri, *internal_id, *external_id;
147
148
  /*
149
   * Anonymous URI are stored with an empty internal ID and the tree
150
   * comparator will make sure they never match each other (so each
151
   * anonymous URI is unique).
152
   */
153
7.04k
  if (internal_id_in == NULL)
154
4.53k
    internal_id_in = "";
155
156
7.04k
  utf8_stravis(&uri, uri_in, VIS_OCTAL|VIS_CSTYLE);
157
7.04k
  if (strlen(uri) > MAX_HYPERLINK_URI) {
158
1
    free(uri);
159
1
    return (0);
160
1
  }
161
162
7.04k
  utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE);
163
7.04k
  if (*internal_id != '\0') {
164
2.51k
    find.uri = uri;
165
2.51k
    find.internal_id = internal_id;
166
167
2.51k
    hlu = RB_FIND(hyperlinks_by_uri_tree, &hl->by_uri, &find);
168
2.51k
    if (hlu != NULL) {
169
141
      free (uri);
170
141
      free (internal_id);
171
141
      return (hlu->inner);
172
141
    }
173
2.51k
  }
174
6.90k
  xasprintf(&external_id, "tmux%llX", hyperlinks_next_external_id++);
175
176
6.90k
  hlu = xcalloc(1, sizeof *hlu);
177
6.90k
  hlu->inner = hl->next_inner++;
178
6.90k
  hlu->internal_id = internal_id;
179
6.90k
  hlu->external_id = external_id;
180
6.90k
  hlu->uri = uri;
181
6.90k
  hlu->tree = hl;
182
6.90k
  RB_INSERT(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
183
6.90k
  RB_INSERT(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
184
185
6.90k
  TAILQ_INSERT_TAIL(&global_hyperlinks, hlu, list_entry);
186
6.90k
  if (++global_hyperlinks_count == MAX_HYPERLINKS)
187
0
    hyperlinks_remove(TAILQ_FIRST(&global_hyperlinks));
188
189
6.90k
  return (hlu->inner);
190
7.04k
}
191
192
/* Get hyperlink by inner number. */
193
int
194
hyperlinks_get(struct hyperlinks *hl, u_int inner, const char **uri_out,
195
    const char **internal_id_out, const char **external_id_out)
196
0
{
197
0
  struct hyperlinks_uri find, *hlu;
198
199
0
  find.inner = inner;
200
201
0
  hlu = RB_FIND(hyperlinks_by_inner_tree, &hl->by_inner, &find);
202
0
  if (hlu == NULL)
203
0
    return (0);
204
0
  if (internal_id_out != NULL)
205
0
    *internal_id_out = hlu->internal_id;
206
0
  if (external_id_out != NULL)
207
0
    *external_id_out = hlu->external_id;
208
0
  *uri_out = hlu->uri;
209
0
  return (1);
210
0
}
211
212
/* Initialize hyperlink set. */
213
struct hyperlinks *
214
hyperlinks_init(void)
215
24.2k
{
216
24.2k
  struct hyperlinks *hl;
217
218
24.2k
  hl = xcalloc(1, sizeof *hl);
219
24.2k
  hl->next_inner = 1;
220
24.2k
  RB_INIT(&hl->by_uri);
221
24.2k
  RB_INIT(&hl->by_inner);
222
24.2k
  hl->references = 1;
223
24.2k
  return (hl);
224
24.2k
}
225
226
/* Copy hyperlink set. */
227
struct hyperlinks *
228
hyperlinks_copy(struct hyperlinks *hl)
229
0
{
230
0
  hl->references++;
231
0
  return (hl);
232
0
}
233
234
/* Free all hyperlinks but not the set itself. */
235
void
236
hyperlinks_reset(struct hyperlinks *hl)
237
24.2k
{
238
24.2k
  struct hyperlinks_uri *hlu, *hlu1;
239
240
24.2k
  RB_FOREACH_SAFE(hlu, hyperlinks_by_inner_tree, &hl->by_inner, hlu1)
241
6.90k
    hyperlinks_remove(hlu);
242
24.2k
}
243
244
/* Free hyperlink set. */
245
void
246
hyperlinks_free(struct hyperlinks *hl)
247
24.2k
{
248
24.2k
  if (--hl->references == 0) {
249
24.2k
    hyperlinks_reset(hl);
250
24.2k
    free(hl);
251
24.2k
  }
252
24.2k
}