| Line | Branch | Exec | Source | 
|---|---|---|---|
| 1 | #include <stddef.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "editorconfig.h" | ||
| 5 | #include "ini.h" | ||
| 6 | #include "match.h" | ||
| 7 | #include "options.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | #include "util/path.h" | ||
| 10 | #include "util/readfile.h" | ||
| 11 | #include "util/string.h" | ||
| 12 | #include "util/string-view.h" | ||
| 13 | #include "util/strtonum.h" | ||
| 14 | #include "util/xstring.h" | ||
| 15 | |||
| 16 | enum { | ||
| 17 | MAX_FILESIZE = 32u << 20, // 32 MiB | ||
| 18 | }; | ||
| 19 | |||
| 20 | typedef struct { | ||
| 21 | const char *const pathname; | ||
| 22 | StringView config_file_dir; | ||
| 23 | EditorConfigOptions options; | ||
| 24 | bool match; | ||
| 25 | } UserData; | ||
| 26 | |||
| 27 | typedef enum { | ||
| 28 | ECONF_CHARSET, | ||
| 29 | ECONF_END_OF_LINE, | ||
| 30 | ECONF_INDENT_SIZE, | ||
| 31 | ECONF_INDENT_STYLE, | ||
| 32 | ECONF_INSERT_FINAL_NL, | ||
| 33 | ECONF_MAX_LINE_LENGTH, | ||
| 34 | ECONF_TAB_WIDTH, | ||
| 35 | ECONF_TRIM_TRAILING_WS, | ||
| 36 | ECONF_UNKNOWN_PROPERTY, | ||
| 37 | } PropertyType; | ||
| 38 | |||
| 39 | #define CMP(s, val) if (mem_equal(name.data, s, STRLEN(s))) return val; | ||
| 40 | |||
| 41 | 9 | static PropertyType lookup_property(StringView name) | |
| 42 | { | ||
| 43 | 
        4/8✗ Branch 0 (2→3) not taken. 
            ✓ Branch 1 (2→6) taken 2 times. 
            ✓ Branch 2 (2→9) taken 3 times. 
            ✓ Branch 3 (2→14) taken 3 times. 
            ✓ Branch 4 (2→17) taken 1 times. 
            ✗ Branch 5 (2→20) not taken. 
            ✗ Branch 6 (2→23) not taken. 
            ✗ Branch 7 (2→26) not taken. 
           | 
      9 | switch (name.length) { | 
| 44 | ✗ | case 7: CMP("charset", ECONF_CHARSET); break; | |
| 45 | 
        1/2✓ Branch 0 (7→8) taken 2 times. 
            ✗ Branch 1 (7→26) not taken. 
           | 
      2 | case 9: CMP("tab_width", ECONF_TAB_WIDTH); break; | 
| 46 | 3 | case 11: | |
| 47 | 
        1/2✗ Branch 0 (10→11) not taken. 
            ✓ Branch 1 (10→26) taken 3 times. 
           | 
      3 | CMP("indent_size", ECONF_INDENT_SIZE); | 
| 48 | ✗ | CMP("end_of_line", ECONF_END_OF_LINE); | |
| 49 | break; | ||
| 50 | 
        1/2✓ Branch 0 (15→16) taken 3 times. 
            ✗ Branch 1 (15→26) not taken. 
           | 
      3 | case 12: CMP("indent_style", ECONF_INDENT_STYLE); break; | 
| 51 | 
        1/2✓ Branch 0 (18→19) taken 1 times. 
            ✗ Branch 1 (18→26) not taken. 
           | 
      1 | case 15: CMP("max_line_length", ECONF_MAX_LINE_LENGTH); break; | 
| 52 | ✗ | case 20: CMP("insert_final_newline", ECONF_INSERT_FINAL_NL); break; | |
| 53 | ✗ | case 24: CMP("trim_trailing_whitespace", ECONF_TRIM_TRAILING_WS); break; | |
| 54 | } | ||
| 55 | return ECONF_UNKNOWN_PROPERTY; | ||
| 56 | } | ||
| 57 | |||
| 58 | 3 | static EditorConfigIndentStyle lookup_indent_style(StringView val) | |
| 59 | { | ||
| 60 | 
        1/2✗ Branch 0 (3→4) not taken. 
            ✓ Branch 1 (3→7) taken 3 times. 
           | 
      3 | if (strview_equal_icase(val, strview("space"))) { | 
| 61 | return INDENT_STYLE_SPACE; | ||
| 62 | ✗ | } else if (strview_equal_icase(val, strview("tab"))) { | |
| 63 | ✗ | return INDENT_STYLE_TAB; | |
| 64 | } | ||
| 65 | return INDENT_STYLE_UNSPECIFIED; | ||
| 66 | } | ||
| 67 | |||
| 68 | 5 | static unsigned int parse_indent_digit(StringView val) | |
| 69 | { | ||
| 70 | 5 | static_assert(INDENT_WIDTH_MAX == TAB_WIDTH_MAX); | |
| 71 | 
        1/2✓ Branch 0 (2→3) taken 5 times. 
            ✗ Branch 1 (2→5) not taken. 
           | 
      5 | unsigned int indent = (val.length == 1) ? val.data[0] - '0' : 0; | 
| 72 | 
        1/2✗ Branch 0 (3→4) not taken. 
            ✓ Branch 1 (3→5) taken 5 times. 
           | 
      5 | return (indent <= INDENT_WIDTH_MAX) ? indent : 0; | 
| 73 | } | ||
| 74 | |||
| 75 | 3 | static void parse_indent_size(EditorConfigOptions *options, StringView val) | |
| 76 | { | ||
| 77 | 3 | bool tab = strview_equal_icase(val, strview("tab")); | |
| 78 | 3 | options->indent_size_is_tab = tab; | |
| 79 | 
        1/2✓ Branch 0 (3→4) taken 3 times. 
            ✗ Branch 1 (3→5) not taken. 
           | 
      3 | options->indent_size = tab ? 0 : parse_indent_digit(val); | 
| 80 | 3 | } | |
| 81 | |||
| 82 | 1 | static unsigned int parse_max_line_length(StringView val) | |
| 83 | { | ||
| 84 | 1 | unsigned int maxlen = 0; | |
| 85 | 1 | size_t ndigits = buf_parse_uint(val.data, val.length, &maxlen); | |
| 86 | 
        1/2✓ Branch 0 (3→4) taken 1 times. 
            ✗ Branch 1 (3→5) not taken. 
           | 
      1 | return (ndigits == val.length) ? MIN(maxlen, TEXT_WIDTH_MAX) : 0; | 
| 87 | } | ||
| 88 | |||
| 89 | 9 | static void editorconfig_option_set ( | |
| 90 | EditorConfigOptions *options, | ||
| 91 | StringView name, | ||
| 92 | StringView val | ||
| 93 | ) { | ||
| 94 | 
        4/6✓ Branch 0 (3→4) taken 3 times. 
            ✓ Branch 1 (3→6) taken 3 times. 
            ✓ Branch 2 (3→7) taken 2 times. 
            ✓ Branch 3 (3→8) taken 1 times. 
            ✗ Branch 4 (3→10) not taken. 
            ✗ Branch 5 (3→12) not taken. 
           | 
      9 | switch (lookup_property(name)) { | 
| 95 | 3 | case ECONF_INDENT_STYLE: | |
| 96 | 3 | options->indent_style = lookup_indent_style(val); | |
| 97 | 3 | break; | |
| 98 | 3 | case ECONF_INDENT_SIZE: | |
| 99 | 3 | parse_indent_size(options, val); | |
| 100 | 3 | break; | |
| 101 | 2 | case ECONF_TAB_WIDTH: | |
| 102 | 2 | options->tab_width = parse_indent_digit(val); | |
| 103 | 2 | break; | |
| 104 | 1 | case ECONF_MAX_LINE_LENGTH: | |
| 105 | 1 | options->max_line_length = parse_max_line_length(val); | |
| 106 | 1 | break; | |
| 107 | case ECONF_CHARSET: | ||
| 108 | case ECONF_END_OF_LINE: | ||
| 109 | case ECONF_INSERT_FINAL_NL: | ||
| 110 | case ECONF_TRIM_TRAILING_WS: | ||
| 111 | case ECONF_UNKNOWN_PROPERTY: | ||
| 112 | break; | ||
| 113 | ✗ | default: | |
| 114 | − | BUG("unhandled property type"); | |
| 115 | } | ||
| 116 | 9 | } | |
| 117 | |||
| 118 | // See: https://editorconfig.org/#wildcards and ec_pattern_match() | ||
| 119 | 296 | static bool is_ec_special_char(char c) | |
| 120 | { | ||
| 121 | 
        1/2✓ Branch 0 (3→4) taken 296 times. 
            ✗ Branch 1 (3→8) not taken. 
           | 
      296 | return c == '*' || c == ',' || c == '-' || c == '?' || c == '[' | 
| 122 | 
        4/8✓ Branch 0 (2→3) taken 296 times. 
            ✗ Branch 1 (2→8) not taken. 
            ✓ Branch 2 (4→5) taken 296 times. 
            ✗ Branch 3 (4→8) not taken. 
            ✓ Branch 4 (5→6) taken 296 times. 
            ✗ Branch 5 (5→8) not taken. 
            ✓ Branch 6 (6→7) taken 296 times. 
            ✗ Branch 7 (6→8) not taken. 
           | 
      592 | || c == ']' || c == '{' || c == '}' || c == '\\'; | 
| 123 | } | ||
| 124 | |||
| 125 | 12 | static bool section_matches_path(StringView section, StringView dir, const char *path) | |
| 126 | { | ||
| 127 | 12 | BUG_ON(section.length == 0); | |
| 128 | 12 | String pattern = string_new(dir.length + section.length + 16); | |
| 129 | |||
| 130 | // Add `dir` prefix to `pattern`, escaping any special characters | ||
| 131 | 
        2/2✓ Branch 0 (10→6) taken 296 times. 
            ✓ Branch 1 (10→11) taken 12 times. 
           | 
      308 | for (size_t i = 0, n = dir.length; i < n; i++) { | 
| 132 | 296 | const char c = dir.data[i]; | |
| 133 | 
        1/2✗ Branch 0 (6→7) not taken. 
            ✓ Branch 1 (6→8) taken 296 times. 
           | 
      296 | if (is_ec_special_char(c)) { | 
| 134 | ✗ | string_append_byte(&pattern, '\\'); | |
| 135 | } | ||
| 136 | 296 | string_append_byte(&pattern, c); | |
| 137 | } | ||
| 138 | |||
| 139 | 
        2/2✓ Branch 0 (11→12) taken 10 times. 
            ✓ Branch 1 (11→13) taken 2 times. 
           | 
      12 | if (!strview_memchr(section, '/')) { | 
| 140 | // Section contains no slashes; insert "**/" between `dir` and `section` | ||
| 141 | 10 | string_append_literal(&pattern, "**/"); | |
| 142 | 
        1/2✓ Branch 0 (13→14) taken 2 times. 
            ✗ Branch 1 (13→15) not taken. 
           | 
      2 | } else if (section.data[0] != '/') { | 
| 143 | // Section contains slashes, but not at the start; insert '/' between | ||
| 144 | // `dir` and `section` | ||
| 145 | 2 | string_append_byte(&pattern, '/'); | |
| 146 | } | ||
| 147 | |||
| 148 | // Append the section heading (from the .editorconfig file) to the | ||
| 149 | // prefix constructed above and test whether the resulting `pattern` | ||
| 150 | // matches against `path` | ||
| 151 | 12 | string_append_strview(&pattern, section); | |
| 152 | 12 | bool r = ec_pattern_match(pattern.buffer, pattern.len, path); | |
| 153 | 12 | string_free(&pattern); | |
| 154 | 12 | return r; | |
| 155 | } | ||
| 156 | |||
| 157 | 4 | static bool is_root_key(const IniParser *ini) | |
| 158 | { | ||
| 159 | 4 | return strview_equal_icase(ini->name, strview("root")) | |
| 160 | 
        2/4✓ Branch 0 (3→4) taken 4 times. 
            ✗ Branch 1 (3→7) not taken. 
            ✗ Branch 2 (5→6) not taken. 
            ✓ Branch 3 (5→7) taken 4 times. 
           | 
      4 | && strview_equal_icase(ini->value, strview("true")); | 
| 161 | } | ||
| 162 | |||
| 163 | 6 | static EditorConfigOptions editorconfig_options_init(void) | |
| 164 | { | ||
| 165 | 6 | return (EditorConfigOptions){.indent_style = INDENT_STYLE_UNSPECIFIED}; | |
| 166 | } | ||
| 167 | |||
| 168 | 4 | static void editorconfig_parse(StringView input, UserData *data) | |
| 169 | { | ||
| 170 | 4 | static const StringView utf8_bom = STRING_VIEW("\xEF\xBB\xBF"); | |
| 171 | 4 | bool has_utf8_bom = strview_has_sv_prefix(input, utf8_bom); | |
| 172 | |||
| 173 | 8 | IniParser ini = { | |
| 174 | .input = input, | ||
| 175 | 
        2/2✓ Branch 0 (3→4) taken 2 times. 
            ✓ Branch 1 (3→5) taken 2 times. 
           | 
      4 | .pos = has_utf8_bom ? utf8_bom.length : 0, | 
| 176 | }; | ||
| 177 | |||
| 178 | 
        2/2✓ Branch 0 (18→6) taken 30 times. 
            ✓ Branch 1 (18→19) taken 4 times. 
           | 
      34 | while (ini_parse(&ini)) { | 
| 179 | 
        2/2✓ Branch 0 (6→7) taken 4 times. 
            ✓ Branch 1 (6→11) taken 26 times. 
           | 
      30 | if (ini.section.length == 0) { | 
| 180 | 
        1/2✓ Branch 0 (8→9) taken 4 times. 
            ✗ Branch 1 (8→10) not taken. 
           | 
      4 | if (is_root_key(&ini)) { | 
| 181 | // root=true, clear all previous values | ||
| 182 | 4 | data->options = editorconfig_options_init(); | |
| 183 | } | ||
| 184 | 4 | continue; | |
| 185 | } | ||
| 186 | |||
| 187 | 
        2/2✓ Branch 0 (11→12) taken 12 times. 
            ✓ Branch 1 (11→14) taken 14 times. 
           | 
      26 | if (ini.name_count == 1) { | 
| 188 | // If name_count is 1, it indicates that the name/value pair is | ||
| 189 | // the first in the section and therefore requires a new pattern | ||
| 190 | // to be built and tested for a match | ||
| 191 | 12 | StringView dir = data->config_file_dir; | |
| 192 | 12 | data->match = section_matches_path(ini.section, dir, data->pathname); | |
| 193 | } else { | ||
| 194 | // Otherwise, the section is the same as was passed for the first | ||
| 195 | // name/value pair in the section and the value of data->match | ||
| 196 | // can just be reused | ||
| 197 | 26 | } | |
| 198 | |||
| 199 | 
        2/2✓ Branch 0 (14→15) taken 9 times. 
            ✓ Branch 1 (14→16) taken 17 times. 
           | 
      26 | if (data->match) { | 
| 200 | 9 | editorconfig_option_set(&data->options, ini.name, ini.value); | |
| 201 | } | ||
| 202 | } | ||
| 203 | 4 | } | |
| 204 | |||
| 205 | 2 | EditorConfigOptions get_editorconfig_options(const char *pathname) | |
| 206 | { | ||
| 207 | 2 | BUG_ON(!path_is_absolute(pathname)); | |
| 208 | 2 | UserData data = { | |
| 209 | .pathname = pathname, | ||
| 210 | .config_file_dir = STRING_VIEW_INIT, | ||
| 211 | 2 | .options = editorconfig_options_init(), | |
| 212 | .match = false, | ||
| 213 | }; | ||
| 214 | |||
| 215 | 2 | static const char ecfilename[16] = "/.editorconfig"; | |
| 216 | 2 | char buf[8192]; | |
| 217 | 2 | memcpy(buf, ecfilename, sizeof ecfilename); | |
| 218 | |||
| 219 | 2 | const char *ptr = pathname + 1; | |
| 220 | 2 | size_t dir_len = 1; | |
| 221 | |||
| 222 | // Iterate up directory tree, looking for ".editorconfig" at each level | ||
| 223 | 22 | while (1) { | |
| 224 | 12 | char *text; | |
| 225 | 12 | ssize_t len = read_file(buf, &text, MAX_FILESIZE); | |
| 226 | 
        2/2✓ Branch 0 (6→7) taken 4 times. 
            ✓ Branch 1 (6→9) taken 8 times. 
           | 
      12 | if (len >= 0) { | 
| 227 | 4 | data.config_file_dir = string_view(buf, dir_len); | |
| 228 | 4 | editorconfig_parse(string_view(text, len), &data); | |
| 229 | 4 | free(text); | |
| 230 | } | ||
| 231 | |||
| 232 | 12 | const char *slash = strchr(ptr, '/'); | |
| 233 | 
        2/2✓ Branch 0 (9→10) taken 10 times. 
            ✓ Branch 1 (9→12) taken 2 times. 
           | 
      12 | if (!slash) { | 
| 234 | break; | ||
| 235 | } | ||
| 236 | |||
| 237 | 10 | dir_len = slash - pathname; | |
| 238 | 10 | xmempcpy2(buf, pathname, dir_len, ecfilename, sizeof ecfilename); | |
| 239 | 10 | ptr = slash + 1; | |
| 240 | } | ||
| 241 | |||
| 242 | // Set indent_size to "tab" if indent_size is not specified and | ||
| 243 | // indent_style is set to "tab" | ||
| 244 | 2 | EditorConfigOptions *o = &data.options; | |
| 245 | 
        3/4✓ Branch 0 (12→13) taken 1 times. 
            ✓ Branch 1 (12→15) taken 1 times. 
            ✗ Branch 2 (13→14) not taken. 
            ✓ Branch 3 (13→15) taken 1 times. 
           | 
      2 | if (o->indent_size == 0 && o->indent_style == INDENT_STYLE_TAB) { | 
| 246 | ✗ | o->indent_size_is_tab = true; | |
| 247 | } | ||
| 248 | |||
| 249 | // Set indent_size to tab_width if indent_size is "tab" and | ||
| 250 | // tab_width is specified | ||
| 251 | 
        1/4✗ Branch 0 (15→16) not taken. 
            ✓ Branch 1 (15→18) taken 2 times. 
            ✗ Branch 2 (16→17) not taken. 
            ✗ Branch 3 (16→18) not taken. 
           | 
      2 | if (o->indent_size_is_tab && o->tab_width > 0) { | 
| 252 | ✗ | o->indent_size = o->tab_width; | |
| 253 | } | ||
| 254 | |||
| 255 | // Set tab_width to indent_size if indent_size is specified as | ||
| 256 | // something other than "tab" and tab_width is unspecified | ||
| 257 | 
        4/6✓ Branch 0 (18→19) taken 1 times. 
            ✓ Branch 1 (18→22) taken 1 times. 
            ✓ Branch 2 (19→20) taken 1 times. 
            ✗ Branch 3 (19→22) not taken. 
            ✓ Branch 4 (20→21) taken 1 times. 
            ✗ Branch 5 (20→22) not taken. 
           | 
      2 | if (o->indent_size != 0 && o->tab_width == 0 && !o->indent_size_is_tab) { | 
| 258 | 1 | o->tab_width = o->indent_size; | |
| 259 | } | ||
| 260 | |||
| 261 | 2 | return data.options; | |
| 262 | } | ||
| 263 |