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