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