dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 92.1% 151 / 1 / 165
Functions: 94.7% 18 / 0 / 19
Branches: 87.8% 86 / 20 / 118

src/filetype.c
Line Branch Exec Source
1 #include <stdint.h>
2 #include <stdlib.h>
3 #include "filetype.h"
4 #include "command/serialize.h"
5 #include "regexp.h"
6 #include "util/ascii.h"
7 #include "util/bsearch.h"
8 #include "util/debug.h"
9 #include "util/hashset.h"
10 #include "util/log.h"
11 #include "util/path.h"
12 #include "util/str-util.h"
13 #include "util/strtonum.h"
14 #include "util/xmalloc.h"
15 #include "util/xmemmem.h"
16
17 2024 static int ft_compare(const void *key, const void *elem)
18 {
19 2024 const StringView *sv = key;
20 2024 const char *ext = elem; // Cast to first member of struct
21 2024 int res = memcmp(sv->data, ext, sv->length);
22
4/4
✓ Branch 2 → 3 taken 156 times.
✓ Branch 2 → 5 taken 1868 times.
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 5 taken 142 times.
2024 if (unlikely(res == 0 && ext[sv->length] != '\0')) {
23 14 res = -1;
24 }
25 2024 return res;
26 }
27
28 // Built-in filetypes
29 // NOLINTBEGIN(bugprone-suspicious-include)
30 #include "filetype/names.c"
31 #include "filetype/basenames.c"
32 #include "filetype/directories.c"
33 #include "filetype/extensions.c"
34 #include "filetype/interpreters.c"
35 #include "filetype/ignored-exts.c"
36 #include "filetype/signatures.c"
37 // NOLINTEND(bugprone-suspicious-include)
38
39 24 UNITTEST {
40 24 static_assert(NR_BUILTIN_FILETYPES < 256);
41 24 CHECK_BSEARCH_ARRAY(basenames, name);
42 24 CHECK_BSEARCH_ARRAY(extensions, ext);
43 24 CHECK_BSEARCH_ARRAY(interpreters, key);
44 24 CHECK_BSEARCH_ARRAY(emacs_modes, name);
45 24 CHECK_BSEARCH_STR_ARRAY(ignored_extensions);
46 24 CHECK_BSEARCH_STR_ARRAY(builtin_filetype_names);
47
48
2/2
✓ Branch 12 → 9 taken 3048 times.
✓ Branch 12 → 13 taken 24 times.
3096 for (size_t i = 0; i < ARRAYLEN(builtin_filetype_names); i++) {
49 3048 const char *name = builtin_filetype_names[i];
50
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3048 times.
3048 if (unlikely(!is_valid_filetype_name(name))) {
51 BUG("invalid name at builtin_filetype_names[%zu]: \"%s\"", i, name);
52 }
53 }
54 24 }
55
56 typedef struct {
57 unsigned int str_len;
58 char str[];
59 } FlexArrayStr;
60
61 // Filetypes dynamically added via the `ft` command.
62 // Not grouped by name to make it possible to order them freely.
63 typedef struct {
64 union {
65 FlexArrayStr *str;
66 const InternedRegexp *regexp;
67 } u;
68 uint8_t type; // FileDetectionType
69 char name[];
70 } UserFileTypeEntry;
71
72 136 static bool ft_uses_regex(FileDetectionType type)
73 {
74 136 return type == FT_CONTENT || type == FT_FILENAME;
75 }
76
77 24 bool add_filetype (
78 PointerArray *filetypes,
79 const char *name,
80 const char *str,
81 FileDetectionType type,
82 ErrorBuffer *ebuf
83 ) {
84 24 BUG_ON(!is_valid_filetype_name(name));
85 24 const InternedRegexp *ir = NULL;
86
2/2
✓ Branch 4 → 5 taken 20 times.
✓ Branch 4 → 7 taken 4 times.
24 if (ft_uses_regex(type)) {
87 20 ir = regexp_intern(ebuf, str);
88
1/2
✓ Branch 6 → 7 taken 20 times.
✗ Branch 6 → 16 not taken.
20 if (unlikely(!ir)) {
89 return false;
90 }
91 }
92
93 24 size_t name_len = strlen(name);
94 24 UserFileTypeEntry *ft = xmalloc(xadd3(sizeof(*ft), name_len, 1));
95 24 ft->type = type;
96
97
2/2
✓ Branch 9 → 10 taken 20 times.
✓ Branch 9 → 11 taken 4 times.
24 if (ir) {
98 20 ft->u.regexp = ir;
99 } else {
100 4 size_t str_len = strlen(str);
101 4 FlexArrayStr *s = xmalloc(xadd3(sizeof(*s), str_len, 1));
102 4 s->str_len = str_len;
103 4 ft->u.str = s;
104 4 memcpy(s->str, str, str_len + 1);
105 }
106
107 24 memcpy(ft->name, name, name_len + 1);
108 24 ptr_array_append(filetypes, ft);
109 24 return true;
110 }
111
112 299 static StringView path_extension(StringView filename)
113 {
114 299 ssize_t last_dot = strview_memrchr_idx(filename, '.');
115
2/2
✓ Branch 2 → 3 taken 117 times.
✓ Branch 2 → 4 taken 182 times.
299 size_t ext_offset = last_dot > 0 ? last_dot + 1 : filename.length;
116 299 return strview_from_slice(filename.data, ext_offset, filename.length);
117 }
118
119 284 static StringView get_filename_extension(StringView filename)
120 {
121 284 StringView ext = path_extension(filename);
122
2/2
✓ Branch 4 → 5 taken 15 times.
✓ Branch 4 → 6 taken 269 times.
284 if (is_ignored_extension(ext)) {
123 15 filename.length -= ext.length + 1;
124 15 ext = path_extension(filename);
125 }
126 284 strview_remove_matching_suffix(&ext, "~");
127 284 return ext;
128 }
129
130 // Parse hashbang and return interpreter name, without version number.
131 // For example, if line is "#!/usr/bin/env python2", "python" is returned.
132 284 static StringView get_interpreter(StringView line)
133 {
134 284 StringView sv = STRING_VIEW_INIT;
135
2/2
✓ Branch 3 → 4 taken 210 times.
✓ Branch 3 → 5 taken 74 times.
284 if (!strview_remove_matching_prefix(&line, "#!")) {
136 210 return sv;
137 }
138
139 74 strview_trim_left(&line);
140
3/4
✓ Branch 6 → 7 taken 74 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 73 times.
74 if (line.length < 2 || line.data[0] != '/') {
141 1 return sv;
142 }
143
144 73 size_t pos = 0;
145 73 sv = get_delim(line.data, &pos, line.length, ' ');
146
4/4
✓ Branch 10 → 11 taken 8 times.
✓ Branch 10 → 17 taken 65 times.
✓ Branch 12 → 14 taken 6 times.
✓ Branch 12 → 17 taken 2 times.
73 if (pos < line.length && strview_equal_cstring(sv, "/usr/bin/env")) {
147
4/4
✓ Branch 14 → 15 taken 6 times.
✓ Branch 14 → 16 taken 1 time.
✓ Branch 15 → 13 taken 1 time.
✓ Branch 15 → 16 taken 5 times.
7 while (pos + 1 < line.length && line.data[pos] == ' ') {
148 1 pos++;
149 }
150 6 sv = get_delim(line.data, &pos, line.length, ' ');
151 }
152
153 73 ssize_t last_slash_idx = strview_memrchr_idx(sv, '/');
154
2/2
✓ Branch 17 → 18 taken 68 times.
✓ Branch 17 → 20 taken 5 times.
73 if (last_slash_idx >= 0) {
155 68 strview_remove_prefix(&sv, last_slash_idx + 1);
156 }
157
158
4/4
✓ Branch 21 → 22 taken 84 times.
✓ Branch 21 → 23 taken 5 times.
✓ Branch 22 → 19 taken 16 times.
✓ Branch 22 → 23 taken 68 times.
89 while (sv.length && ascii_is_digit_or_dot(sv.data[sv.length - 1])) {
159 16 sv.length--;
160 }
161
162 73 return sv;
163 }
164
165 25 static bool ft_str_match(const UserFileTypeEntry *ft, StringView sv)
166 {
167 25 const FlexArrayStr *s = ft->u.str;
168
4/4
✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 6 taken 9 times.
✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 6 taken 3 times.
25 return sv.length && strview_equal(sv, string_view(s->str, s->str_len));
169 }
170
171 60 static bool ft_regex_match(const UserFileTypeEntry *ft, const StringView sv)
172 {
173 60 const regex_t *re = &ft->u.regexp->re;
174
4/4
✓ Branch 2 → 3 taken 54 times.
✓ Branch 2 → 6 taken 6 times.
✓ Branch 4 → 5 taken 52 times.
✓ Branch 4 → 6 taken 2 times.
60 return sv.length > 0 && regexp_exec(re, sv.data, sv.length, 0, NULL, 0);
175 }
176
177 85 static bool ft_match(const UserFileTypeEntry *ft, const StringView sv)
178 {
179 85 FileDetectionType t = ft->type;
180
2/2
✓ Branch 2 → 3 taken 60 times.
✓ Branch 2 → 4 taken 25 times.
85 return ft_uses_regex(t) ? ft_regex_match(ft, sv) : ft_str_match(ft, sv);
181 }
182
183 typedef FileTypeEnum (*FileTypeLookupFunc)(const StringView sv);
184
185 284 const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line)
186 {
187 284 const StringView path = strview(filename);
188 284 const StringView base = path_slice_basename(path);
189 284 const StringView ext = get_filename_extension(base);
190 284 const StringView interpreter = get_interpreter(line);
191 284 BUG_ON(path.length == 0 && (base.length != 0 || ext.length != 0));
192 284 BUG_ON(line.length == 0 && interpreter.length != 0);
193
194 // The order of elements in this array determines the order of
195 // precedence for the lookup() functions (but note that changing
196 // the initializer below makes no difference to the array order)
197 284 static const FileTypeLookupFunc funcs[] = {
198 [FT_INTERPRETER] = filetype_from_interpreter,
199 [FT_BASENAME] = filetype_from_basename,
200 [FT_CONTENT] = filetype_from_signature,
201 [FT_EXTENSION] = filetype_from_extension,
202 [FT_FILENAME] = filetype_from_path,
203 };
204
205 284 const StringView params[] = {
206 [FT_INTERPRETER] = interpreter,
207 [FT_BASENAME] = base,
208 [FT_CONTENT] = line,
209 [FT_EXTENSION] = ext,
210 [FT_FILENAME] = path,
211 };
212
213 // Search user `ft` entries
214
2/2
✓ Branch 17 → 13 taken 85 times.
✓ Branch 17 → 24 taken 279 times.
364 for (size_t i = 0, n = filetypes->count; i < n; i++) {
215 85 const UserFileTypeEntry *ft = filetypes->ptrs[i];
216
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 16 taken 80 times.
85 if (ft_match(ft, params[ft->type])) {
217 5 return ft->name;
218 }
219 }
220
221 // Search built-in lookup tables
222 static_assert(ARRAYLEN(funcs) == ARRAYLEN(params));
223
2/2
✓ Branch 24 → 18 taken 929 times.
✓ Branch 24 → 25 taken 94 times.
1023 for (FileDetectionType i = 0; i < ARRAYLEN(funcs); i++) {
224 929 BUG_ON(!funcs[i]);
225 929 FileTypeEnum ft = funcs[i](params[i]);
226
2/2
✓ Branch 21 → 22 taken 185 times.
✓ Branch 21 → 23 taken 744 times.
929 if (ft != NONE) {
227 185 return builtin_filetype_names[ft];
228 }
229 }
230
231 // Search lower precedence file signatures
232 94 FileTypeEnum ft = filetype_from_signature_late(line);
233
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 28 taken 92 times.
94 if (ft != NONE) {
234 2 return builtin_filetype_names[ft];
235 }
236
237
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 36 taken 92 times.
92 if (strview_equal_cstring(ext, "conf")) {
238 BUG_ON(!filename);
239 const StringView dir = path_slice_dirname(filename);
240 if (strview_has_suffix(dir, "/tmpfiles.d")) {
241 return builtin_filetype_names[CONFIG];
242 }
243 }
244
245 return NULL;
246 }
247
248 31 bool is_ft(const PointerArray *filetypes, const char *name)
249 {
250
2/2
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 8 taken 17 times.
31 if (BSEARCH(name, builtin_filetype_names, vstrcmp)) {
251 return true;
252 }
253
254
2/2
✓ Branch 7 → 5 taken 26 times.
✓ Branch 7 → 8 taken 8 times.
34 for (size_t i = 0, n = filetypes->count; i < n; i++) {
255 26 const UserFileTypeEntry *ft = filetypes->ptrs[i];
256
2/2
✓ Branch 5 → 6 taken 20 times.
✓ Branch 5 → 8 taken 6 times.
26 if (streq(ft->name, name)) {
257 return true;
258 }
259 }
260
261 return false;
262 }
263
264 2 void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix)
265 {
266 // Insert all filetype names beginning with `prefix` into a HashSet
267 // (to avoid duplicates)
268 2 HashSet set;
269 2 size_t prefix_len = strlen(prefix);
270 2 size_t nr_builtin_ft = ARRAYLEN(builtin_filetype_names);
271
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
2 hashset_init(&set, 20 + (prefix[0] == '\0' ? nr_builtin_ft : 0), false);
272
273
2/2
✓ Branch 9 → 6 taken 254 times.
✓ Branch 9 → 10 taken 2 times.
258 for (size_t i = 0; i < nr_builtin_ft; i++) {
274 254 const char *name = builtin_filetype_names[i];
275
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 252 times.
254 if (str_has_strn_prefix(name, prefix, prefix_len)) {
276 2 hashset_insert(&set, name, strlen(name));
277 }
278 }
279
280
1/2
✗ Branch 14 → 11 not taken.
✓ Branch 14 → 15 taken 2 times.
2 for (size_t i = 0, n = filetypes->count; i < n; i++) {
281 const UserFileTypeEntry *ft = filetypes->ptrs[i];
282 const char *name = ft->name;
283 if (str_has_strn_prefix(name, prefix, prefix_len)) {
284 hashset_insert(&set, name, strlen(name));
285 }
286 }
287
288 // Append the collected strings to the PointerArray
289
2/2
✓ Branch 19 → 16 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
4 for (HashSetIter iter = hashset_iter(&set); hashset_next(&iter); ) {
290 2 ptr_array_append(a, xmemdup(iter.entry->str, iter.entry->str_len + 1));
291 }
292
293 2 hashset_free(&set);
294 2 }
295
296 3 static const char *ft_get_str(const UserFileTypeEntry *ft)
297 {
298
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1 time.
3 return ft_uses_regex(ft->type) ? ft->u.regexp->str : ft->u.str->str;
299 }
300
301 1 String dump_filetypes(const PointerArray *filetypes)
302 {
303 1 static const char flags[][4] = {
304 [FT_EXTENSION] = "",
305 [FT_FILENAME] = "-f ",
306 [FT_CONTENT] = "-c ",
307 [FT_INTERPRETER] = "-i ",
308 [FT_BASENAME] = "-b ",
309 };
310
311 1 String s = string_new(4096);
312
2/2
✓ Branch 15 → 4 taken 3 times.
✓ Branch 15 → 16 taken 1 time.
4 for (size_t i = 0, n = filetypes->count; i < n; i++) {
313 3 const UserFileTypeEntry *ft = filetypes->ptrs[i];
314 3 BUG_ON(ft->type >= ARRAYLEN(flags));
315 3 BUG_ON(ft->name[0] == '-');
316 3 string_append_literal(&s, "ft ");
317 3 string_append_cstring(&s, flags[ft->type]);
318 3 string_append_escaped_arg(&s, ft->name, true);
319 3 string_append_byte(&s, ' ');
320 3 string_append_escaped_arg(&s, ft_get_str(ft), true);
321 3 string_append_byte(&s, '\n');
322 }
323 1 return s;
324 }
325
326 24 static void free_filetype_entry(UserFileTypeEntry *ft)
327 {
328
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 20 times.
24 if (!ft_uses_regex(ft->type)) {
329 4 free(ft->u.str);
330 }
331 24 free(ft);
332 24 }
333
334 12 void free_filetypes(PointerArray *filetypes)
335 {
336 12 ptr_array_free_cb(filetypes, FREE_FUNC(free_filetype_entry));
337 12 }
338
339 3525 bool is_valid_filetype_name_sv(StringView name)
340 {
341 3525 const char *data = name.data;
342 3525 const size_t len = name.length;
343
4/4
✓ Branch 2 → 3 taken 3516 times.
✓ Branch 2 → 8 taken 9 times.
✓ Branch 3 → 7 taken 3513 times.
✓ Branch 3 → 8 taken 3 times.
3525 if (unlikely(len == 0 || len > FILETYPE_NAME_MAX || data[0] == '-')) {
344 return false;
345 }
346
347 const AsciiCharType mask = ASCII_SPACE | ASCII_CNTRL;
348
2/2
✓ Branch 7 → 4 taken 17994 times.
✓ Branch 7 → 8 taken 3502 times.
21496 for (size_t i = 0; i < len; i++) {
349 17994 unsigned char ch = data[i];
350
4/4
✓ Branch 4 → 5 taken 17985 times.
✓ Branch 4 → 8 taken 9 times.
✓ Branch 5 → 6 taken 17983 times.
✓ Branch 5 → 8 taken 2 times.
17994 if (unlikely(ascii_test(ch, mask) || ch == '/')) {
351 return false;
352 }
353 }
354
355 return true;
356 }
357
358 const char *filetype_str_from_extension(const char *path)
359 {
360 StringView base = path_slice_basename(strview(path));
361 StringView ext = get_filename_extension(base);
362 FileTypeEnum ft = filetype_from_extension(ext);
363 return (ft == NONE) ? NULL : builtin_filetype_names[ft];
364 }
365