dte test coverage


Directory: ./
File: src/command/parse.c
Date: 2025-07-13 15:27:15
Exec Total Coverage
Lines: 163 163 100.0%
Functions: 11 11 100.0%
Branches: 84 86 97.7%

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/string.h"
9 #include "util/strtonum.h"
10 #include "util/unicode.h"
11 #include "util/xmalloc.h"
12 #include "util/xstring.h"
13
14 1623 static size_t parse_sq(const char *cmd, size_t len, String *buf)
15 {
16 1623 const char *end = memchr(cmd, '\'', len);
17
2/2
✓ Branch 0 (2→3) taken 1622 times.
✓ Branch 1 (2→4) taken 1 times.
1623 size_t pos = end ? (size_t)(end - cmd) : len;
18 1623 string_append_buf(buf, cmd, pos);
19
2/2
✓ Branch 0 (5→6) taken 1 times.
✓ Branch 1 (5→7) taken 1622 times.
1623 return pos + (end ? 1 : 0);
20 }
21
22 6 static size_t unicode_escape(const char *str, size_t count, String *buf)
23 {
24 // Note: `u` doesn't need to be initialized here, but `gcc -Og`
25 // gives a spurious -Wmaybe-uninitialized warning if it's not
26 6 unsigned int u = 0;
27 6 static_assert(sizeof(u) >= 4);
28 6 size_t n = buf_parse_hex_uint(str, count, &u);
29
3/4
✓ Branch 0 (3→4) taken 4 times.
✓ Branch 1 (3→6) taken 2 times.
✓ Branch 2 (4→5) taken 4 times.
✗ Branch 3 (4→6) not taken.
6 if (likely(n > 0 && u_is_unicode(u))) {
30 4 string_append_codepoint(buf, u);
31 }
32 6 return n;
33 }
34
35 44 static size_t hex_escape(const char *str, size_t count, String *buf)
36 {
37 44 unsigned int x = 0;
38 44 size_t n = buf_parse_hex_uint(str, count, &x);
39
2/2
✓ Branch 0 (3→4) taken 39 times.
✓ Branch 1 (3→5) taken 5 times.
44 if (likely(n == 2)) {
40 39 string_append_byte(buf, x);
41 }
42 44 return n;
43 }
44
45 669 static size_t parse_dq(const char *cmd, size_t len, String *buf)
46 {
47 669 size_t pos = 0;
48
2/2
✓ Branch 0 (24→3) taken 2984 times.
✓ Branch 1 (24→25) taken 2 times.
2986 while (pos < len) {
49 2984 unsigned char ch = cmd[pos++];
50
2/2
✓ Branch 0 (3→4) taken 2317 times.
✓ Branch 1 (3→25) taken 667 times.
2984 if (ch == '"') {
51 break;
52 }
53
2/2
✓ Branch 0 (4→5) taken 668 times.
✓ Branch 1 (4→22) taken 1649 times.
2317 if (ch == '\\' && pos < len) {
54 668 ch = cmd[pos++];
55
13/13
✓ Branch 0 (5→6) taken 1 times.
✓ Branch 1 (5→7) taken 1 times.
✓ Branch 2 (5→8) taken 1 times.
✓ Branch 3 (5→9) taken 3 times.
✓ Branch 4 (5→10) taken 369 times.
✓ Branch 5 (5→11) taken 9 times.
✓ Branch 6 (5→12) taken 119 times.
✓ Branch 7 (5→13) taken 1 times.
✓ Branch 8 (5→14) taken 44 times.
✓ Branch 9 (5→16) taken 4 times.
✓ Branch 10 (5→18) taken 2 times.
✓ Branch 11 (5→20) taken 5 times.
✓ Branch 12 (5→22) taken 109 times.
668 switch (ch) {
56 1 case 'a': ch = '\a'; break;
57 1 case 'b': ch = '\b'; break;
58 1 case 'e': ch = '\033'; break;
59 3 case 'f': ch = '\f'; break;
60 369 case 'n': ch = '\n'; break;
61 9 case 'r': ch = '\r'; break;
62 119 case 't': ch = '\t'; break;
63 1 case 'v': ch = '\v'; break;
64 case '\\':
65 case '"':
66 break;
67 44 case 'x':
68 44 pos += hex_escape(cmd + pos, MIN(2, len - pos), buf);
69 44 continue;
70 4 case 'u':
71 4 pos += unicode_escape(cmd + pos, MIN(4, len - pos), buf);
72 4 continue;
73 2 case 'U':
74 2 pos += unicode_escape(cmd + pos, MIN(8, len - pos), buf);
75 2 continue;
76 5 default:
77 5 string_append_byte(buf, '\\');
78 5 break;
79 }
80 }
81 2267 string_append_byte(buf, ch);
82 }
83
84 669 return pos;
85 }
86
87 27 static size_t expand_var (
88 const CommandRunner *runner,
89 const char *cmd,
90 size_t len,
91 String *buf
92 ) {
93
4/4
✓ Branch 0 (2→3) taken 26 times.
✓ Branch 1 (2→9) taken 1 times.
✓ Branch 2 (3→4) taken 25 times.
✓ Branch 3 (3→9) taken 1 times.
27 if (!runner->expand_variable || len == 0) {
94 return len;
95 }
96
97 25 char *name = xstrcut(cmd, len);
98 25 char *value = runner->expand_variable(runner->e, name);
99 25 free(name);
100
2/2
✓ Branch 0 (6→7) taken 16 times.
✓ Branch 1 (6→9) taken 9 times.
25 if (value) {
101 16 string_append_cstring(buf, value);
102 16 free(value);
103 }
104
105 return len;
106 }
107
108 // Handles bracketed variables, e.g. ${varname}
109 3 static size_t parse_bracketed_var (
110 const CommandRunner *runner,
111 const char *cmd,
112 size_t len,
113 String *buf
114 ) {
115 3 const char *end = memchr(cmd + 1, '}', len - 1);
116
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→5) taken 2 times.
3 if (!end) {
117 1 LOG_WARNING("no end delimiter for bracketed variable: $%.*s", (int)len, cmd);
118 1 return len; // Consume without appending
119 }
120
121 2 size_t var_len = (size_t)(end - cmd) - 1;
122 2 TRACE_CMD("expanding variable: ${%.*s}", (int)var_len, cmd + 1);
123 2 return expand_var(runner, cmd + 1, var_len, buf) + STRLEN("{}");
124 }
125
126 31 static size_t parse_var (
127 const CommandRunner *runner,
128 const char *cmd,
129 size_t len,
130 String *buf
131 ) {
132
1/2
✓ Branch 0 (2→3) taken 31 times.
✗ Branch 1 (2→4) not taken.
31 char ch = len ? cmd[0] : 0;
133
2/2
✓ Branch 0 (4→5) taken 6 times.
✓ Branch 1 (4→8) taken 25 times.
31 if (!is_alpha_or_underscore(ch)) {
134 6 bool bracketed = (ch == '{');
135
2/2
✓ Branch 0 (5→6) taken 3 times.
✓ Branch 1 (5→7) taken 3 times.
6 return bracketed ? parse_bracketed_var(runner, cmd, len, buf) : 0;
136 }
137
138 25 AsciiCharType type_mask = ASCII_ALNUM | ASCII_UNDERSCORE;
139 25 size_t var_len = ascii_type_prefix_length(cmd, len, type_mask);
140 25 TRACE_CMD("expanding variable: $%.*s", (int)var_len, cmd);
141 25 return expand_var(runner, cmd, 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 32732 char *parse_command_arg(const CommandRunner *runner, const char *cmd, size_t len)
150 {
151 32732 const StringView *home = runner->home_dir;
152 32732 bool expand_ts = (runner->flags & CMDRUNNER_EXPAND_TILDE_SLASH);
153
4/4
✓ Branch 0 (2→3) taken 31831 times.
✓ Branch 1 (2→6) taken 901 times.
✓ Branch 2 (4→5) taken 3 times.
✓ Branch 3 (4→6) taken 31828 times.
32732 bool tilde_slash = expand_ts && len >= 2 && mem_equal(cmd, "~/", 2);
154 32732 String buf = string_new(len + 1 + (tilde_slash ? home->length : 0));
155 32732 size_t pos = 0;
156
157
2/2
✓ Branch 0 (7→8) taken 3 times.
✓ Branch 1 (7→21) taken 32729 times.
32732 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 0 (24→9) taken 180314 times.
✓ Branch 1 (24→25) taken 32726 times.
213040 while (pos < len) {
163 180314 char ch = cmd[pos++];
164
6/6
✓ Branch 0 (9→10) taken 5 times.
✓ Branch 1 (9→11) taken 1623 times.
✓ Branch 2 (9→13) taken 669 times.
✓ Branch 3 (9→15) taken 31 times.
✓ Branch 4 (9→17) taken 239 times.
✓ Branch 5 (9→20) taken 177747 times.
180314 switch (ch) {
165 5 case '\t':
166 case '\n':
167 case '\r':
168 case ' ':
169 case ';':
170 5 goto end;
171 1623 case '\'':
172 1623 pos += parse_sq(cmd + pos, len - pos, &buf);
173 1623 break;
174 669 case '"':
175 669 pos += parse_dq(cmd + pos, len - pos, &buf);
176 669 break;
177 31 case '$':
178 31 pos += parse_var(runner, cmd + pos, len - pos, &buf);
179 31 break;
180 239 case '\\':
181
2/2
✓ Branch 0 (17→18) taken 1 times.
✓ Branch 1 (17→19) taken 238 times.
239 if (unlikely(pos == len)) {
182 1 goto end;
183 }
184 238 ch = cmd[pos++];
185 // Fallthrough
186 177985 default:
187 177985 string_append_byte(&buf, ch);
188 177985 break;
189 }
190 }
191
192 32726 end:
193 32732 return string_steal_cstring(&buf);
194 }
195
196 32680 size_t find_end(const char *cmd, size_t pos, CommandParseError *err)
197 {
198 212994 while (1) {
199
5/5
✓ Branch 0 (5→5) taken 177821 times.
✓ Branch 1 (5→6) taken 1613 times.
✓ Branch 2 (5→11) taken 658 times.
✓ Branch 3 (5→20) taken 236 times.
✓ Branch 4 (5→23) taken 32666 times.
212994 switch (cmd[pos++]) {
200 28050 case '\'':
201 54487 while (1) {
202
2/2
✓ Branch 0 (6→7) taken 1608 times.
✓ Branch 1 (6→8) taken 26442 times.
28050 if (cmd[pos] == '\'') {
203 1608 pos++;
204 1608 break;
205 }
206
2/2
✓ Branch 0 (8→9) taken 5 times.
✓ Branch 1 (8→10) taken 26437 times.
26442 if (unlikely(cmd[pos] == '\0')) {
207 5 *err = CMDERR_UNCLOSED_SQUOTE;
208 5 return 0;
209 }
210 26437 pos++;
211 }
212 1608 break;
213 1300 case '"':
214 3000 while (1) {
215
2/2
✓ Branch 0 (12→13) taken 651 times.
✓ Branch 1 (12→14) taken 2349 times.
3000 if (cmd[pos] == '"') {
216 651 pos++;
217 651 break;
218 }
219
2/2
✓ Branch 0 (14→15) taken 5 times.
✓ Branch 1 (14→16) taken 2344 times.
2349 if (unlikely(cmd[pos] == '\0')) {
220 5 *err = CMDERR_UNCLOSED_DQUOTE;
221 5 return 0;
222 }
223
2/2
✓ Branch 0 (16→12) taken 1700 times.
✓ Branch 1 (16→17) taken 644 times.
2344 if (cmd[pos++] == '\\') {
224
2/2
✓ Branch 0 (17→18) taken 2 times.
✓ Branch 1 (17→19) taken 642 times.
644 if (unlikely(cmd[pos] == '\0')) {
225 2 *err = CMDERR_UNEXPECTED_EOF;
226 2 return 0;
227 }
228 642 pos++;
229 }
230 }
231 651 break;
232 236 case '\\':
233
2/2
✓ Branch 0 (20→21) taken 2 times.
✓ Branch 1 (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 32666 case '\0':
240 case '\t':
241 case '\n':
242 case '\r':
243 case ' ':
244 case ';':
245 32666 *err = CMDERR_NONE;
246 32666 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 11827 CommandParseError parse_commands (
255 const CommandRunner *runner,
256 PointerArray *array,
257 const char *cmd
258 ) {
259 11827 for (size_t pos = 0; true; ) {
260
2/2
✓ Branch 0 (5→3) taken 22124 times.
✓ Branch 1 (5→6) taken 44375 times.
66499 while (ascii_isspace(cmd[pos])) {
261 22124 pos++;
262 }
263
264
2/2
✓ Branch 0 (6→7) taken 32562 times.
✓ Branch 1 (6→16) taken 11813 times.
44375 if (cmd[pos] == '\0') {
265 break;
266 }
267
268
2/2
✓ Branch 0 (7→8) taken 105 times.
✓ Branch 1 (7→10) taken 32457 times.
32562 if (cmd[pos] == ';') {
269 105 ptr_array_append(array, NULL);
270 105 pos++;
271 105 continue;
272 }
273
274 32457 CommandParseError err;
275 32457 size_t end = find_end(cmd, pos, &err);
276
2/2
✓ Branch 0 (11→12) taken 14 times.
✓ Branch 1 (11→13) taken 32443 times.
32457 if (err != CMDERR_NONE) {
277 14 return err;
278 }
279
280 32443 ptr_array_append(array, parse_command_arg(runner, cmd + pos, end - pos));
281 32443 pos = end;
282 }
283
284 11813 ptr_array_append(array, NULL);
285 11813 return CMDERR_NONE;
286 }
287
288 6 const char *command_parse_error_to_string(CommandParseError err)
289 {
290 6 static const char error_strings[][16] = {
291 [CMDERR_UNCLOSED_SQUOTE] = "unclosed '",
292 [CMDERR_UNCLOSED_DQUOTE] = "unclosed \"",
293 [CMDERR_UNEXPECTED_EOF] = "unexpected EOF",
294 };
295
296 6 BUG_ON(err <= CMDERR_NONE);
297 6 BUG_ON(err >= ARRAYLEN(error_strings));
298 6 return error_strings[err];
299 }
300