dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 100.0% 162 / 0 / 162
Functions: 100.0% 12 / 0 / 12
Branches: 96.5% 82 / 4 / 89

src/command/parse.c
Line Branch Exec Source
1 #include <stdbool.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include "parse.h"
5 #include "trace.h"
6 #include "util/ascii.h"
7 #include "util/debug.h"
8 #include "util/str-util.h"
9 #include "util/strtonum.h"
10 #include "util/unicode.h"
11 #include "util/xmalloc.h"
12
13 1646 static size_t parse_sq(StringView cmd, String *buf)
14 {
15 1646 size_t pos = 0;
16 1646 string_append_strview(buf, get_delim(cmd.data, &pos, cmd.length, '\''));
17 1646 return pos;
18 }
19
20 6 static size_t unicode_escape(StringView hexstr, String *buf)
21 {
22 // Note: `u` doesn't need to be initialized here, but `gcc -Og`
23 // gives a spurious -Wmaybe-uninitialized warning if it's not
24 6 unsigned int u = 0;
25 6 static_assert(sizeof(u) >= 4);
26 6 size_t n = buf_parse_hex_uint(hexstr, &u);
27
3/4
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
6 if (likely(n > 0 && u_is_unicode(u))) {
28 4 string_append_codepoint(buf, u);
29 }
30 6 return n;
31 }
32
33 44 static size_t hex_escape(StringView hexstr, String *buf)
34 {
35 44 unsigned int x = 0;
36 44 size_t n = buf_parse_hex_uint(hexstr, &x);
37
2/2
✓ Branch 3 → 4 taken 39 times.
✓ Branch 3 → 5 taken 5 times.
44 if (likely(n == 2)) {
38 39 string_append_byte(buf, x);
39 }
40 44 return n;
41 }
42
43 50 static StringView slice(StringView cmd, size_t pos, size_t maxlen)
44 {
45 50 return string_view(cmd.data + pos, MIN(maxlen, cmd.length - pos));
46 }
47
48 694 static size_t parse_dq(StringView cmd, String *buf)
49 {
50 694 size_t pos = 0;
51
2/2
✓ Branch 25 → 3 taken 3972 times.
✓ Branch 25 → 26 taken 2 times.
3974 while (pos < cmd.length) {
52 3972 unsigned char ch = cmd.data[pos++];
53
2/2
✓ Branch 3 → 4 taken 3280 times.
✓ Branch 3 → 26 taken 692 times.
3972 if (ch == '"') {
54 break;
55 }
56
3/4
✓ Branch 4 → 5 taken 695 times.
✓ Branch 4 → 23 taken 2585 times.
✓ Branch 5 → 6 taken 695 times.
✗ Branch 5 → 23 not taken.
3280 if (ch == '\\' && pos < cmd.length) {
57 695 ch = cmd.data[pos++];
58
12/12
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 6 → 10 taken 3 times.
✓ Branch 6 → 11 taken 390 times.
✓ Branch 6 → 12 taken 9 times.
✓ Branch 6 → 13 taken 125 times.
✓ Branch 6 → 14 taken 1 time.
✓ Branch 6 → 15 taken 44 times.
✓ Branch 6 → 17 taken 6 times.
✓ Branch 6 → 21 taken 5 times.
✓ Branch 6 → 23 taken 109 times.
695 switch (ch) {
59 1 case 'a': ch = '\a'; break;
60 1 case 'b': ch = '\b'; break;
61 1 case 'e': ch = '\033'; break;
62 3 case 'f': ch = '\f'; break;
63 390 case 'n': ch = '\n'; break;
64 9 case 'r': ch = '\r'; break;
65 125 case 't': ch = '\t'; break;
66 1 case 'v': ch = '\v'; break;
67 case '\\':
68 case '"':
69 break;
70 44 case 'x':
71 44 pos += hex_escape(slice(cmd, pos, 2), buf);
72 44 continue;
73 6 case 'u':
74 case 'U':
75
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 2 times.
10 pos += unicode_escape(slice(cmd, pos, ch == 'U' ? 8 : 4), buf);
76 6 continue;
77 5 default:
78 5 string_append_byte(buf, '\\');
79 5 break;
80 }
81 }
82 3230 string_append_byte(buf, ch);
83 }
84
85 694 return pos;
86 }
87
88 27 static size_t expand_var (
89 const CommandRunner *runner,
90 const char *cmd,
91 size_t len,
92 String *buf
93 ) {
94
4/4
✓ Branch 2 → 3 taken 26 times.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 3 → 4 taken 25 times.
✓ Branch 3 → 9 taken 1 time.
27 if (!runner->expand_variable || len == 0) {
95 return len;
96 }
97
98 25 char *name = xstrcut(cmd, len);
99 25 char *value = runner->expand_variable(runner->e, name);
100 25 free(name);
101
2/2
✓ Branch 6 → 7 taken 16 times.
✓ Branch 6 → 9 taken 9 times.
25 if (value) {
102 16 string_append_cstring(buf, value);
103 16 free(value);
104 }
105
106 return len;
107 }
108
109 // Handles bracketed variables, e.g. ${varname}
110 3 static size_t parse_bracketed_var (
111 const CommandRunner *runner,
112 const char *cmd,
113 size_t len,
114 String *buf
115 ) {
116 3 const char *end = memchr(cmd + 1, '}', len - 1);
117
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 2 times.
3 if (!end) {
118 1 LOG_WARNING("no end delimiter for bracketed variable: $%.*s", (int)len, cmd);
119 1 return len; // Consume without appending
120 }
121
122 2 size_t var_len = (size_t)(end - cmd) - 1;
123 2 TRACE_CMD("expanding variable: ${%.*s}", (int)var_len, cmd + 1);
124 2 return expand_var(runner, cmd + 1, var_len, buf) + STRLEN("{}");
125 }
126
127 31 static size_t parse_var (
128 const CommandRunner *runner,
129 StringView cmd,
130 String *buf
131 ) {
132
1/2
✓ Branch 2 → 3 taken 31 times.
✗ Branch 2 → 4 not taken.
31 char ch = cmd.length ? cmd.data[0] : 0;
133
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 7 taken 25 times.
31 if (!is_alpha_or_underscore(ch)) {
134 6 bool bracketed = (ch == '{');
135
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 9 taken 3 times.
6 return bracketed ? parse_bracketed_var(runner, cmd.data, cmd.length, buf) : 0;
136 }
137
138 25 AsciiCharType type_mask = ASCII_ALNUM | ASCII_UNDERSCORE;
139 25 size_t var_len = ascii_type_prefix_length(cmd.data, cmd.length, type_mask);
140 25 TRACE_CMD("expanding variable: $%.*s", (int)var_len, cmd.data);
141 25 return expand_var(runner, cmd.data, var_len, buf);
142 }
143
144 // Parse a single dterc(5) argument from `cmd`, stopping when an unquoted
145 // whitespace or semicolon character is found or when all `len` bytes have
146 // been processed without encountering such a character. Escape sequences
147 // and $variables are expanded during processing and the fully expanded
148 // result is returned as a malloc'd string.
149 33623 String parse_command_arg(const CommandRunner *runner, StringView cmd)
150 {
151 33623 const StringView *home = runner->home_dir;
152 33623 bool expand_ts = (runner->flags & CMDRUNNER_EXPAND_TILDE_SLASH);
153
4/4
✓ Branch 2 → 3 taken 33535 times.
✓ Branch 2 → 6 taken 88 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 33532 times.
33623 bool tilde_slash = expand_ts && strview_has_prefix(cmd, "~/");
154 33623 String buf = string_new(cmd.length + 1 + (tilde_slash ? home->length : 0));
155 33623 size_t pos = 0;
156
157
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 25 taken 33620 times.
33623 if (tilde_slash) {
158 3 string_append_strview(&buf, *home);
159 3 pos += 1; // Skip past '~' and leave '/' to be handled below
160 }
161
162
2/2
✓ Branch 28 → 10 taken 185129 times.
✓ Branch 28 → 29 taken 33617 times.
218746 while (pos < cmd.length) {
163 185129 char ch = cmd.data[pos++];
164
6/6
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 1646 times.
✓ Branch 10 → 15 taken 694 times.
✓ Branch 10 → 18 taken 31 times.
✓ Branch 10 → 21 taken 239 times.
✓ Branch 10 → 24 taken 182514 times.
185129 switch (ch) {
165 5 case '\t':
166 case '\n':
167 case '\r':
168 case ' ':
169 case ';':
170 5 goto end;
171 1646 case '\'':
172 1646 pos += parse_sq(strview_from_slice(cmd.data, pos, cmd.length), &buf);
173 1646 break;
174 694 case '"':
175 694 pos += parse_dq(strview_from_slice(cmd.data, pos, cmd.length), &buf);
176 694 break;
177 31 case '$':
178 31 pos += parse_var(runner, strview_from_slice(cmd.data, pos, cmd.length), &buf);
179 31 break;
180 239 case '\\':
181
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 238 times.
239 if (unlikely(pos == cmd.length)) {
182 1 goto end;
183 }
184 238 ch = cmd.data[pos++];
185 // Fallthrough
186 182752 default:
187 182752 string_append_byte(&buf, ch);
188 182752 break;
189 }
190 }
191
192 33617 end:
193 33623 return buf;
194 }
195
196 33572 size_t find_end(const char *cmd, size_t pos, CommandParseError *err)
197 {
198 218709 while (1) {
199
5/5
✓ Branch 5 → 5 taken 182596 times.
✓ Branch 5 → 6 taken 1636 times.
✓ Branch 5 → 11 taken 683 times.
✓ Branch 5 → 20 taken 236 times.
✓ Branch 5 → 23 taken 33558 times.
218709 switch (cmd[pos++]) {
200 28390 case '\'':
201 55144 while (1) {
202
2/2
✓ Branch 6 → 7 taken 1631 times.
✓ Branch 6 → 8 taken 26759 times.
28390 if (cmd[pos] == '\'') {
203 1631 pos++;
204 1631 break;
205 }
206
2/2
✓ Branch 8 → 9 taken 5 times.
✓ Branch 8 → 10 taken 26754 times.
26759 if (unlikely(cmd[pos] == '\0')) {
207 5 *err = CMDERR_UNCLOSED_SQUOTE;
208 5 return 0;
209 }
210 26754 pos++;
211 }
212 1631 break;
213 1352 case '"':
214 3988 while (1) {
215
2/2
✓ Branch 12 → 13 taken 676 times.
✓ Branch 12 → 14 taken 3312 times.
3988 if (cmd[pos] == '"') {
216 676 pos++;
217 676 break;
218 }
219
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 16 taken 3307 times.
3312 if (unlikely(cmd[pos] == '\0')) {
220 5 *err = CMDERR_UNCLOSED_DQUOTE;
221 5 return 0;
222 }
223
2/2
✓ Branch 16 → 12 taken 2636 times.
✓ Branch 16 → 17 taken 671 times.
3307 if (cmd[pos++] == '\\') {
224
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 669 times.
671 if (unlikely(cmd[pos] == '\0')) {
225 2 *err = CMDERR_UNEXPECTED_EOF;
226 2 return 0;
227 }
228 669 pos++;
229 }
230 }
231 676 break;
232 236 case '\\':
233
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 234 times.
236 if (unlikely(cmd[pos] == '\0')) {
234 2 *err = CMDERR_UNEXPECTED_EOF;
235 2 return 0;
236 }
237 234 pos++;
238 234 break;
239 33558 case '\0':
240 case '\t':
241 case '\n':
242 case '\r':
243 case ' ':
244 case ';':
245 33558 *err = CMDERR_NONE;
246 33558 return pos - 1;
247 }
248 }
249
250 BUG("Unexpected break of outer loop");
251 }
252
253 // Note: `array` must be freed, regardless of the return value
254 12161 CommandParseError parse_commands (
255 const CommandRunner *runner,
256 PointerArray *array,
257 const char *cmd
258 ) {
259 12161 for (size_t pos = 0; true; ) {
260
2/2
✓ Branch 5 → 3 taken 22710 times.
✓ Branch 5 → 6 taken 45597 times.
68307 while (ascii_isspace(cmd[pos])) {
261 22710 pos++;
262 }
263
264
2/2
✓ Branch 6 → 7 taken 33450 times.
✓ Branch 6 → 18 taken 12147 times.
45597 if (cmd[pos] == '\0') {
265 break;
266 }
267
268
2/2
✓ Branch 7 → 8 taken 105 times.
✓ Branch 7 → 10 taken 33345 times.
33450 if (cmd[pos] == ';') {
269 105 ptr_array_append(array, NULL);
270 105 pos++;
271 105 continue;
272 }
273
274 33345 CommandParseError err;
275 33345 size_t end = find_end(cmd, pos, &err);
276
2/2
✓ Branch 11 → 12 taken 14 times.
✓ Branch 11 → 13 taken 33331 times.
33345 if (err != CMDERR_NONE) {
277 14 return err;
278 }
279
280 33331 String arg = parse_command_arg(runner, strview_from_slice(cmd, pos, end));
281 33331 ptr_array_append(array, string_steal_cstring(&arg));
282 33331 pos = end;
283 }
284
285 12147 ptr_array_append(array, NULL);
286 12147 return CMDERR_NONE;
287 }
288
289 6 const char *command_parse_error_to_string(CommandParseError err)
290 {
291 6 static const char error_strings[][16] = {
292 [CMDERR_UNCLOSED_SQUOTE] = "unclosed '",
293 [CMDERR_UNCLOSED_DQUOTE] = "unclosed \"",
294 [CMDERR_UNEXPECTED_EOF] = "unexpected EOF",
295 };
296
297 6 BUG_ON(err <= CMDERR_NONE);
298 6 BUG_ON(err >= ARRAYLEN(error_strings));
299 6 return error_strings[err];
300 }
301