1 /*
2  * hyperlink.c
3  * Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
4  *
5  * Distributed under terms of the GPL3 license.
6  */
7 
8 #include "hyperlink.h"
9 #include "kitty-uthash.h"
10 #include <string.h>
11 
12 #define MAX_KEY_LEN 2048
13 #define MAX_ID_LEN 256
14 #define MAX_ADDS_BEFORE_GC 256
15 
16 typedef struct {
17     const char *key;
18     hyperlink_id_type id;
19     UT_hash_handle hh;
20 } HyperLinkEntry;
21 
22 
23 typedef struct {
24     HyperLinkEntry *hyperlinks;
25     unsigned int max_link_id, num_of_adds_since_garbage_collection;
26 } HyperLinkPool;
27 
28 
29 static void
free_hyperlink_entry(HyperLinkEntry * s)30 free_hyperlink_entry(HyperLinkEntry *s) {
31     free((void*)s->key);
32     free(s);
33 }
34 
35 static void
clear_pool(HyperLinkPool * pool)36 clear_pool(HyperLinkPool *pool) {
37     if (pool->hyperlinks) {
38         HyperLinkEntry *tmp, *s;
39         HASH_ITER(hh, pool->hyperlinks, s, tmp) {
40             HASH_DEL(pool->hyperlinks, s);
41             free_hyperlink_entry(s); s = NULL;
42         }
43         pool->max_link_id = 0;
44     }
45 }
46 
47 HYPERLINK_POOL_HANDLE
alloc_hyperlink_pool(void)48 alloc_hyperlink_pool(void) {
49     return calloc(1, sizeof(HyperLinkPool));
50 }
51 
52 
53 void
clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h)54 clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
55     if (h) {
56         HyperLinkPool *pool = (HyperLinkPool*)h;
57         clear_pool(pool);
58     }
59 }
60 
61 
62 void
free_hyperlink_pool(HYPERLINK_POOL_HANDLE h)63 free_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
64     if (h) {
65         HyperLinkPool *pool = (HyperLinkPool*)h;
66         clear_pool(pool);
67         free(pool);
68     }
69 }
70 
71 
72 void
screen_garbage_collect_hyperlink_pool(Screen * screen)73 screen_garbage_collect_hyperlink_pool(Screen *screen) {
74     HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
75     pool->num_of_adds_since_garbage_collection = 0;
76     if (!pool->max_link_id) return;
77     hyperlink_id_type *map = calloc(HYPERLINK_MAX_NUMBER + 4, sizeof(hyperlink_id_type));
78     if (!map) fatal("Out of memory");
79     hyperlink_id_type num = remap_hyperlink_ids(screen, map);
80     if (num) {
81         HyperLinkEntry *s, *tmp;
82         pool->max_link_id = 0;
83         HASH_ITER(hh, pool->hyperlinks, s, tmp) {
84             if (map[s->id]) {
85                 s->id = map[s->id];
86                 pool->max_link_id = MAX(pool->max_link_id, s->id);
87             } else {
88                 HASH_DEL(pool->hyperlinks, s);
89                 free_hyperlink_entry(s); s = NULL;
90             }
91         }
92     } else clear_pool(pool);
93     free(map);
94 }
95 
96 
97 hyperlink_id_type
get_id_for_hyperlink(Screen * screen,const char * id,const char * url)98 get_id_for_hyperlink(Screen *screen, const char *id, const char *url) {
99     if (!url) return 0;
100     HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
101     static char key[MAX_KEY_LEN] = {0};
102     int keylen = snprintf(key, MAX_KEY_LEN-1, "%.*s:%s", MAX_ID_LEN, id ? id : "", url);
103     if (keylen < 0) keylen = strlen(key);
104     else keylen = MIN(keylen, MAX_KEY_LEN - 2);  // snprintf returns how many chars it would have written in case of truncation
105     key[keylen] = 0;
106     HyperLinkEntry *s = NULL;
107     if (pool->hyperlinks) {
108         HASH_FIND_STR(pool->hyperlinks, key, s);
109         if (s) {
110             // Remove and re-add s so that it is the last entry in the hash table and
111             // The first entry is discarded when hash table is full.
112             HASH_DEL(pool->hyperlinks, s);
113             HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, strlen(s->key), s);
114             return s->id;
115         }
116     }
117     hyperlink_id_type new_id = 0;
118     if (pool->num_of_adds_since_garbage_collection >= MAX_ADDS_BEFORE_GC) screen_garbage_collect_hyperlink_pool(screen);
119     if (pool->max_link_id >= HYPERLINK_MAX_NUMBER && pool->hyperlinks) {
120         log_error("Too many hyperlinks, discarding oldest, this means some hyperlinks might be incorrect");
121         new_id = pool->hyperlinks->id;
122         HyperLinkEntry *s = pool->hyperlinks;
123         HASH_DEL(pool->hyperlinks, s);
124         free_hyperlink_entry(s); s = NULL;
125     }
126     s = malloc(sizeof(HyperLinkEntry));
127     if (!s) fatal("Out of memory");
128     s->key = malloc(keylen + 1);
129     if (!s->key) fatal("Out of memory");
130     memcpy((void*)s->key, key, keylen + 1);
131     s->id = new_id ? new_id : ++pool->max_link_id;
132     HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, keylen, s);
133     pool->num_of_adds_since_garbage_collection++;
134     return s->id;
135 }
136 
137 const char*
get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle,hyperlink_id_type id,bool only_url)138 get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle, hyperlink_id_type id, bool only_url) {
139     const HyperLinkPool *pool = (HyperLinkPool*)handle;
140     HyperLinkEntry *s, *tmp;
141     HASH_ITER(hh, pool->hyperlinks, s, tmp) {
142         if (s->id == id) return only_url ? strstr(s->key, ":") + 1 : s->key;
143     }
144     return NULL;
145 }
146 
147 
148 PyObject*
screen_hyperlinks_as_list(Screen * screen)149 screen_hyperlinks_as_list(Screen *screen) {
150     HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
151     PyObject *ans = PyList_New(0);
152     HyperLinkEntry *s, *tmp;
153     HASH_ITER(hh, pool->hyperlinks, s, tmp) {
154         PyObject *e = Py_BuildValue("sH", s->key, s->id);
155         PyList_Append(ans, e);
156         Py_DECREF(e);
157     }
158     return ans;
159 }
160