summaryrefslogtreecommitdiffstats
path: root/plugins/check_dig.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/check_dig.c')
-rw-r--r--plugins/check_dig.c455
1 files changed, 363 insertions, 92 deletions
diff --git a/plugins/check_dig.c b/plugins/check_dig.c
index 2bbd1e05..9ea19e6a 100644
--- a/plugins/check_dig.c
+++ b/plugins/check_dig.c
@@ -3,7 +3,7 @@
3 * Monitoring check_dig plugin 3 * Monitoring check_dig plugin
4 * 4 *
5 * License: GPL 5 * License: GPL
6 * Copyright (c) 2002-2024 Monitoring Plugins Development Team 6 * Copyright (c) 2002-2025 Monitoring Plugins Development Team
7 * 7 *
8 * Description: 8 * Description:
9 * 9 *
@@ -33,105 +33,132 @@
33 * because on some architectures those strings are in non-writable memory */ 33 * because on some architectures those strings are in non-writable memory */
34 34
35const char *progname = "check_dig"; 35const char *progname = "check_dig";
36const char *copyright = "2002-2024"; 36const char *copyright = "2002-2025";
37const char *email = "devel@monitoring-plugins.org"; 37const char *email = "devel@monitoring-plugins.org";
38 38
39#include <ctype.h>
39#include "common.h" 40#include "common.h"
40#include "netutils.h" 41#include "netutils.h"
41#include "utils.h" 42#include "utils.h"
42#include "runcmd.h" 43#include "runcmd.h"
43 44
44static int process_arguments(int /*argc*/, char ** /*argv*/); 45#include "check_dig.d/config.h"
45static int validate_arguments(void); 46#include "states.h"
47
48typedef struct {
49 int errorcode;
50 check_dig_config config;
51} check_dig_config_wrapper;
52static check_dig_config_wrapper process_arguments(int /*argc*/, char ** /*argv*/);
53static check_dig_config_wrapper validate_arguments(check_dig_config_wrapper /*config_wrapper*/);
54
46static void print_help(void); 55static void print_help(void);
47void print_usage(void); 56void print_usage(void);
48 57
49#define UNDEFINED 0 58static int verbose = 0;
50#define DEFAULT_PORT 53
51#define DEFAULT_TRIES 2
52
53static char *query_address = NULL;
54static char *record_type = "A";
55static char *expected_address = NULL;
56static char *dns_server = NULL;
57static char *dig_args = "";
58static char *query_transport = "";
59static bool verbose = false;
60static int server_port = DEFAULT_PORT;
61static int number_tries = DEFAULT_TRIES;
62static double warning_interval = UNDEFINED;
63static double critical_interval = UNDEFINED;
64static struct timeval tv;
65 59
66int main(int argc, char **argv) { 60/* helpers for flag parsing */
67 char *command_line; 61static flag_list parse_flags_line(const char *line);
68 output chld_out; 62static flag_list split_csv_trim(const char *csv);
69 output chld_err; 63static bool flag_list_contains(const flag_list *list, const char *needle);
70 char *msg = NULL; 64static void free_flag_list(flag_list *list);
71 size_t i;
72 char *t;
73 long microsec;
74 double elapsed_time;
75 int result = STATE_UNKNOWN;
76 int timeout_interval_dig;
77 65
66int main(int argc, char **argv) {
78 setlocale(LC_ALL, ""); 67 setlocale(LC_ALL, "");
79 bindtextdomain(PACKAGE, LOCALEDIR); 68 bindtextdomain(PACKAGE, LOCALEDIR);
80 textdomain(PACKAGE); 69 textdomain(PACKAGE);
81 70
82 /* Set signal handling and alarm */ 71 /* Set signal handling and alarm */
83 if (signal(SIGALRM, runcmd_timeout_alarm_handler) == SIG_ERR) 72 if (signal(SIGALRM, runcmd_timeout_alarm_handler) == SIG_ERR) {
84 usage_va(_("Cannot catch SIGALRM")); 73 usage_va(_("Cannot catch SIGALRM"));
74 }
85 75
86 /* Parse extra opts if any */ 76 /* Parse extra opts if any */
87 argv = np_extra_opts(&argc, argv, progname); 77 argv = np_extra_opts(&argc, argv, progname);
88 78
89 if (process_arguments(argc, argv) == ERROR) 79 check_dig_config_wrapper tmp_config = process_arguments(argc, argv);
80 if (tmp_config.errorcode == ERROR) {
90 usage_va(_("Could not parse arguments")); 81 usage_va(_("Could not parse arguments"));
82 }
83
84 const check_dig_config config = tmp_config.config;
91 85
92 /* dig applies the timeout to each try, so we need to work around this */ 86 /* dig applies the timeout to each try, so we need to work around this */
93 timeout_interval_dig = timeout_interval / number_tries + number_tries; 87 int timeout_interval_dig = ((int)timeout_interval / config.number_tries) + config.number_tries;
94 88
89 char *command_line;
95 /* get the command to run */ 90 /* get the command to run */
96 xasprintf(&command_line, "%s %s %s -p %d @%s %s %s +retry=%d +time=%d", PATH_TO_DIG, dig_args, query_transport, server_port, dns_server, 91 xasprintf(&command_line, "%s %s %s -p %d @%s %s %s +retry=%d +time=%d", PATH_TO_DIG,
97 query_address, record_type, number_tries, timeout_interval_dig); 92 config.dig_args, config.query_transport, config.server_port, config.dns_server,
93 config.query_address, config.record_type, config.number_tries, timeout_interval_dig);
98 94
99 alarm(timeout_interval); 95 alarm(timeout_interval);
100 gettimeofday(&tv, NULL); 96 struct timeval start_time;
97 gettimeofday(&start_time, NULL);
101 98
102 if (verbose) { 99 if (verbose) {
103 printf("%s\n", command_line); 100 printf("%s\n", command_line);
104 if (expected_address != NULL) { 101 if (config.expected_address != NULL) {
105 printf(_("Looking for: '%s'\n"), expected_address); 102 printf(_("Looking for: '%s'\n"), config.expected_address);
106 } else { 103 } else {
107 printf(_("Looking for: '%s'\n"), query_address); 104 printf(_("Looking for: '%s'\n"), config.query_address);
108 } 105 }
109 } 106 }
110 107
108 output chld_out;
109 output chld_err;
110 char *msg = NULL;
111 flag_list dig_flags = {.items = NULL, .count = 0};
112 mp_state_enum result = STATE_UNKNOWN;
113
111 /* run the command */ 114 /* run the command */
112 if (np_runcmd(command_line, &chld_out, &chld_err, 0) != 0) { 115 if (np_runcmd(command_line, &chld_out, &chld_err, 0) != 0) {
113 result = STATE_WARNING; 116 result = STATE_WARNING;
114 msg = (char *)_("dig returned an error status"); 117 msg = (char *)_("dig returned an error status");
115 } 118 }
116 119
117 for (i = 0; i < chld_out.lines; i++) { 120 /* extract ';; flags: ...' from stdout (first occurrence) */
121 for (size_t i = 0; i < chld_out.lines; i++) {
122 if (strstr(chld_out.line[i], "flags:")) {
123 if (verbose) {
124 printf("Raw flags line: %s\n", chld_out.line[i]);
125 }
126
127 dig_flags = parse_flags_line(chld_out.line[i]);
128
129 if (verbose && dig_flags.count > 0) {
130 printf(_("Parsed flags:"));
131 for (size_t k = 0; k < dig_flags.count; k++) {
132 printf(" %s", dig_flags.items[k]);
133 }
134 printf("\n");
135 }
136 break;
137 }
138 }
139
140 for (size_t i = 0; i < chld_out.lines; i++) {
118 /* the server is responding, we just got the host name... */ 141 /* the server is responding, we just got the host name... */
119 if (strstr(chld_out.line[i], ";; ANSWER SECTION:")) { 142 if (strstr(chld_out.line[i], ";; ANSWER SECTION:")) {
120 143
121 /* loop through the whole 'ANSWER SECTION' */ 144 /* loop through the whole 'ANSWER SECTION' */
122 for (; i < chld_out.lines; i++) { 145 for (; i < chld_out.lines; i++) {
123 /* get the host address */ 146 /* get the host address */
124 if (verbose) 147 if (verbose) {
125 printf("%s\n", chld_out.line[i]); 148 printf("%s\n", chld_out.line[i]);
149 }
126 150
127 if (strcasestr(chld_out.line[i], (expected_address == NULL ? query_address : expected_address)) != NULL) { 151 if (strcasestr(chld_out.line[i], (config.expected_address == NULL
152 ? config.query_address
153 : config.expected_address)) != NULL) {
128 msg = chld_out.line[i]; 154 msg = chld_out.line[i];
129 result = STATE_OK; 155 result = STATE_OK;
130 156
131 /* Translate output TAB -> SPACE */ 157 /* Translate output TAB -> SPACE */
132 t = msg; 158 char *temp = msg;
133 while ((t = strchr(t, '\t')) != NULL) 159 while ((temp = strchr(temp, '\t')) != NULL) {
134 *t = ' '; 160 *temp = ' ';
161 }
135 break; 162 break;
136 } 163 }
137 } 164 }
@@ -154,43 +181,84 @@ int main(int argc, char **argv) {
154 /* If we get anything on STDERR, at least set warning */ 181 /* If we get anything on STDERR, at least set warning */
155 if (chld_err.buflen > 0) { 182 if (chld_err.buflen > 0) {
156 result = max_state(result, STATE_WARNING); 183 result = max_state(result, STATE_WARNING);
157 if (!msg) 184 if (!msg) {
158 for (i = 0; i < chld_err.lines; i++) { 185 for (size_t i = 0; i < chld_err.lines; i++) {
159 msg = strchr(chld_err.line[0], ':'); 186 msg = strchr(chld_err.line[0], ':');
160 if (msg) { 187 if (msg) {
161 msg++; 188 msg++;
162 break; 189 break;
163 } 190 }
164 } 191 }
192 }
165 } 193 }
166 194
167 microsec = deltime(tv); 195 long microsec = deltime(start_time);
168 elapsed_time = (double)microsec / 1.0e6; 196 double elapsed_time = (double)microsec / 1.0e6;
169 197
170 if (critical_interval > UNDEFINED && elapsed_time > critical_interval) 198 if (config.critical_interval > UNDEFINED && elapsed_time > config.critical_interval) {
171 result = STATE_CRITICAL; 199 result = STATE_CRITICAL;
200 }
172 201
173 else if (warning_interval > UNDEFINED && elapsed_time > warning_interval) 202 else if (config.warning_interval > UNDEFINED && elapsed_time > config.warning_interval) {
174 result = STATE_WARNING; 203 result = STATE_WARNING;
204 }
205
206 /* Optional: evaluate dig flags only if -E/-X were provided */
207 if ((config.require_flags.count > 0) || (config.forbid_flags.count > 0)) {
208 if (dig_flags.count > 0) {
209 for (size_t r = 0; r < config.require_flags.count; r++) {
210 if (!flag_list_contains(&dig_flags, config.require_flags.items[r])) {
211 result = STATE_CRITICAL;
212 if (!msg) {
213 xasprintf(&msg, _("Missing required DNS flag: %s"),
214 config.require_flags.items[r]);
215 } else {
216 char *newmsg = NULL;
217 xasprintf(&newmsg, _("%s; missing required DNS flag: %s"), msg,
218 config.require_flags.items[r]);
219 msg = newmsg;
220 }
221 }
222 }
223
224 for (size_t r = 0; r < config.forbid_flags.count; r++) {
225 if (flag_list_contains(&dig_flags, config.forbid_flags.items[r])) {
226 result = STATE_CRITICAL;
227 if (!msg) {
228 xasprintf(&msg, _("Forbidden DNS flag present: %s"),
229 config.forbid_flags.items[r]);
230 } else {
231 char *newmsg = NULL;
232 xasprintf(&newmsg, _("%s; forbidden DNS flag present: %s"), msg,
233 config.forbid_flags.items[r]);
234 msg = newmsg;
235 }
236 }
237 }
238 }
239 }
240
241 /* cleanup flags buffer */
242 free_flag_list(&dig_flags);
175 243
176 printf("DNS %s - %.3f seconds response time (%s)|%s\n", state_text(result), elapsed_time, 244 printf("DNS %s - %.3f seconds response time (%s)|%s\n", state_text(result), elapsed_time,
177 msg ? msg : _("Probably a non-existent host/domain"), 245 msg ? msg : _("Probably a non-existent host/domain"),
178 fperfdata("time", elapsed_time, "s", (warning_interval > UNDEFINED ? true : false), warning_interval, 246 fperfdata("time", elapsed_time, "s", (config.warning_interval > UNDEFINED),
179 (critical_interval > UNDEFINED ? true : false), critical_interval, true, 0, false, 0)); 247 config.warning_interval, (config.critical_interval > UNDEFINED),
180 return result; 248 config.critical_interval, true, 0, false, 0));
249 exit(result);
181} 250}
182 251
183/* process command-line arguments */ 252/* process command-line arguments */
184int process_arguments(int argc, char **argv) { 253check_dig_config_wrapper process_arguments(int argc, char **argv) {
185 int c;
186
187 int option = 0;
188 static struct option longopts[] = {{"hostname", required_argument, 0, 'H'}, 254 static struct option longopts[] = {{"hostname", required_argument, 0, 'H'},
189 {"query_address", required_argument, 0, 'l'}, 255 {"query_address", required_argument, 0, 'l'},
190 {"warning", required_argument, 0, 'w'}, 256 {"warning", required_argument, 0, 'w'},
191 {"critical", required_argument, 0, 'c'}, 257 {"critical", required_argument, 0, 'c'},
192 {"timeout", required_argument, 0, 't'}, 258 {"timeout", required_argument, 0, 't'},
193 {"dig-arguments", required_argument, 0, 'A'}, 259 {"dig-arguments", required_argument, 0, 'A'},
260 {"require-flags", required_argument, 0, 'E'},
261 {"forbid-flags", required_argument, 0, 'X'},
194 {"verbose", no_argument, 0, 'v'}, 262 {"verbose", no_argument, 0, 'v'},
195 {"version", no_argument, 0, 'V'}, 263 {"version", no_argument, 0, 'V'},
196 {"help", no_argument, 0, 'h'}, 264 {"help", no_argument, 0, 'h'},
@@ -201,16 +269,26 @@ int process_arguments(int argc, char **argv) {
201 {"use-ipv6", no_argument, 0, '6'}, 269 {"use-ipv6", no_argument, 0, '6'},
202 {0, 0, 0, 0}}; 270 {0, 0, 0, 0}};
203 271
204 if (argc < 2) 272 check_dig_config_wrapper result = {
205 return ERROR; 273 .errorcode = OK,
274 .config = check_dig_config_init(),
275 };
276
277 if (argc < 2) {
278 result.errorcode = ERROR;
279 return result;
280 }
206 281
207 while (1) { 282 int option = 0;
208 c = getopt_long(argc, argv, "hVvt:l:H:w:c:T:p:a:A:46", longopts, &option); 283 while (true) {
284 int option_index =
285 getopt_long(argc, argv, "hVvt:l:H:w:c:T:p:a:A:E:X:46", longopts, &option);
209 286
210 if (c == -1 || c == EOF) 287 if (option_index == -1 || option_index == EOF) {
211 break; 288 break;
289 }
212 290
213 switch (c) { 291 switch (option_index) {
214 case 'h': /* help */ 292 case 'h': /* help */
215 print_help(); 293 print_help();
216 exit(STATE_UNKNOWN); 294 exit(STATE_UNKNOWN);
@@ -219,28 +297,28 @@ int process_arguments(int argc, char **argv) {
219 exit(STATE_UNKNOWN); 297 exit(STATE_UNKNOWN);
220 case 'H': /* hostname */ 298 case 'H': /* hostname */
221 host_or_die(optarg); 299 host_or_die(optarg);
222 dns_server = optarg; 300 result.config.dns_server = optarg;
223 break; 301 break;
224 case 'p': /* server port */ 302 case 'p': /* server port */
225 if (is_intpos(optarg)) { 303 if (is_intpos(optarg)) {
226 server_port = atoi(optarg); 304 result.config.server_port = atoi(optarg);
227 } else { 305 } else {
228 usage_va(_("Port must be a positive integer - %s"), optarg); 306 usage_va(_("Port must be a positive integer - %s"), optarg);
229 } 307 }
230 break; 308 break;
231 case 'l': /* address to lookup */ 309 case 'l': /* address to lookup */
232 query_address = optarg; 310 result.config.query_address = optarg;
233 break; 311 break;
234 case 'w': /* warning */ 312 case 'w': /* warning */
235 if (is_nonnegative(optarg)) { 313 if (is_nonnegative(optarg)) {
236 warning_interval = strtod(optarg, NULL); 314 result.config.warning_interval = strtod(optarg, NULL);
237 } else { 315 } else {
238 usage_va(_("Warning interval must be a positive integer - %s"), optarg); 316 usage_va(_("Warning interval must be a positive integer - %s"), optarg);
239 } 317 }
240 break; 318 break;
241 case 'c': /* critical */ 319 case 'c': /* critical */
242 if (is_nonnegative(optarg)) { 320 if (is_nonnegative(optarg)) {
243 critical_interval = strtod(optarg, NULL); 321 result.config.critical_interval = strtod(optarg, NULL);
244 } else { 322 } else {
245 usage_va(_("Critical interval must be a positive integer - %s"), optarg); 323 usage_va(_("Critical interval must be a positive integer - %s"), optarg);
246 } 324 }
@@ -253,48 +331,56 @@ int process_arguments(int argc, char **argv) {
253 } 331 }
254 break; 332 break;
255 case 'A': /* dig arguments */ 333 case 'A': /* dig arguments */
256 dig_args = strdup(optarg); 334 result.config.dig_args = strdup(optarg);
335 break;
336 case 'E': /* require flags */
337 result.config.require_flags = split_csv_trim(optarg);
338 break;
339 case 'X': /* forbid flags */
340 result.config.forbid_flags = split_csv_trim(optarg);
257 break; 341 break;
258 case 'v': /* verbose */ 342 case 'v': /* verbose */
259 verbose = true; 343 verbose++;
260 break; 344 break;
261 case 'T': 345 case 'T':
262 record_type = optarg; 346 result.config.record_type = optarg;
263 break; 347 break;
264 case 'a': 348 case 'a':
265 expected_address = optarg; 349 result.config.expected_address = optarg;
266 break; 350 break;
267 case '4': 351 case '4':
268 query_transport = "-4"; 352 result.config.query_transport = "-4";
269 break; 353 break;
270 case '6': 354 case '6':
271 query_transport = "-6"; 355 result.config.query_transport = "-6";
272 break; 356 break;
273 default: /* usage5 */ 357 default: /* usage5 */
274 usage5(); 358 usage5();
275 } 359 }
276 } 360 }
277 361
278 c = optind; 362 int index = optind;
279 if (dns_server == NULL) { 363 if (result.config.dns_server == NULL) {
280 if (c < argc) { 364 if (index < argc) {
281 host_or_die(argv[c]); 365 host_or_die(argv[index]);
282 dns_server = argv[c]; 366 result.config.dns_server = argv[index];
283 } else { 367 } else {
284 if (strcmp(query_transport, "-6") == 0) 368 if (strcmp(result.config.query_transport, "-6") == 0) {
285 dns_server = strdup("::1"); 369 result.config.dns_server = strdup("::1");
286 else 370 } else {
287 dns_server = strdup("127.0.0.1"); 371 result.config.dns_server = strdup("127.0.0.1");
372 }
288 } 373 }
289 } 374 }
290 375
291 return validate_arguments(); 376 return validate_arguments(result);
292} 377}
293 378
294int validate_arguments(void) { 379check_dig_config_wrapper validate_arguments(check_dig_config_wrapper config_wrapper) {
295 if (query_address != NULL) 380 if (config_wrapper.config.query_address == NULL) {
296 return OK; 381 config_wrapper.errorcode = ERROR;
297 return ERROR; 382 }
383 return config_wrapper;
298} 384}
299 385
300void print_help(void) { 386void print_help(void) {
@@ -328,10 +414,15 @@ void print_help(void) {
328 printf(" %s\n", "-T, --record_type=STRING"); 414 printf(" %s\n", "-T, --record_type=STRING");
329 printf(" %s\n", _("Record type to lookup (default: A)")); 415 printf(" %s\n", _("Record type to lookup (default: A)"));
330 printf(" %s\n", "-a, --expected_address=STRING"); 416 printf(" %s\n", "-a, --expected_address=STRING");
331 printf(" %s\n", _("An address expected to be in the answer section. If not set, uses whatever")); 417 printf(" %s\n",
418 _("An address expected to be in the answer section. If not set, uses whatever"));
332 printf(" %s\n", _("was in -l")); 419 printf(" %s\n", _("was in -l"));
333 printf(" %s\n", "-A, --dig-arguments=STRING"); 420 printf(" %s\n", "-A, --dig-arguments=STRING");
334 printf(" %s\n", _("Pass STRING as argument(s) to dig")); 421 printf(" %s\n", _("Pass STRING as argument(s) to dig"));
422 printf(" %s\n", "-E, --require-flags=LIST");
423 printf(" %s\n", _("Comma-separated dig flags that must be present (e.g. 'aa,qr')"));
424 printf(" %s\n", "-X, --forbid-flags=LIST");
425 printf(" %s\n", _("Comma-separated dig flags that must NOT be present"));
335 printf(UT_WARN_CRIT); 426 printf(UT_WARN_CRIT);
336 printf(UT_CONN_TIMEOUT, DEFAULT_SOCKET_TIMEOUT); 427 printf(UT_CONN_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
337 printf(UT_VERBOSE); 428 printf(UT_VERBOSE);
@@ -348,5 +439,185 @@ void print_usage(void) {
348 printf("%s\n", _("Usage:")); 439 printf("%s\n", _("Usage:"));
349 printf("%s -l <query_address> [-H <host>] [-p <server port>]\n", progname); 440 printf("%s -l <query_address> [-H <host>] [-p <server port>]\n", progname);
350 printf(" [-T <query type>] [-w <warning interval>] [-c <critical interval>]\n"); 441 printf(" [-T <query type>] [-w <warning interval>] [-c <critical interval>]\n");
351 printf(" [-t <timeout>] [-a <expected answer address>] [-v]\n"); 442 printf(" [-t <timeout>] [-a <expected answer address>] [-E <flags>] [-X <flags>] [-v]\n");
443}
444
445/* helpers */
446
447/**
448 * parse_flags_line - Parse a dig output line and extract DNS header flags.
449 *
450 * Input:
451 * line - NUL terminated dig output line, e.g. ";; flags: qr rd ra; ..."
452 *
453 * Returns:
454 * flag_list where:
455 * - items: array of NUL terminated flag strings (heap allocated)
456 * - count: number of entries in items
457 * On parse failure or if no flags were found, count is 0 and items is NULL.
458 */
459static flag_list parse_flags_line(const char *line) {
460 flag_list result = {.items = NULL, .count = 0};
461
462 if (!line) {
463 return result;
464 }
465
466 /* Locate start of DNS header flags in dig output */
467 const char *p = strstr(line, "flags:");
468 if (!p) {
469 return result;
470 }
471 p += 6; /* skip literal "flags:" */
472
473 /* Skip whitespace after "flags:" */
474 while (*p && isspace((unsigned char)*p)) {
475 p++;
476 }
477
478 /* Flags are terminated by the next semicolon e.g. "qr rd ra;" */
479 const char *q = strchr(p, ';');
480 if (!q) {
481 return result;
482 }
483
484 /* Extract substring containing the flag block */
485 size_t len = (size_t)(q - p);
486 if (len == 0) {
487 return result;
488 }
489
490 char *buf = (char *)malloc(len + 1);
491 if (!buf) {
492 return result;
493 }
494 memcpy(buf, p, len);
495 buf[len] = '\0';
496
497 /* Tokenize flags separated by whitespace */
498 char **arr = NULL;
499 size_t cnt = 0;
500 char *saveptr = NULL;
501 char *tok = strtok_r(buf, " \t", &saveptr);
502
503 while (tok) {
504 /* Expand array for the next flag token */
505 char **tmp = (char **)realloc(arr, (cnt + 1) * sizeof(char *));
506 if (!tmp) {
507 /* On allocation failure keep what we have and return it */
508 break;
509 }
510 arr = tmp;
511 arr[cnt++] = strdup(tok);
512 tok = strtok_r(NULL, " \t", &saveptr);
513 }
514
515 free(buf);
516
517 result.items = arr;
518 result.count = cnt;
519 return result;
520}
521
522/**
523 * split_csv_trim - Split a comma separated string into trimmed tokens.
524 *
525 * Input:
526 * csv - NUL terminated string, e.g. "aa, qr , rd"
527 *
528 * Returns:
529 * flag_list where:
530 * - items: array of NUL terminated tokens (heap allocated, whitespace trimmed)
531 * - count: number of tokens
532 * On empty input, count is 0 and items is NULL
533 */
534static flag_list split_csv_trim(const char *csv) {
535 flag_list result = {.items = NULL, .count = 0};
536
537 if (!csv || !*csv) {
538 return result;
539 }
540
541 char *tmp = strdup(csv);
542 if (!tmp) {
543 return result;
544 }
545
546 char *s = tmp;
547 char *token = NULL;
548
549 /* Split CSV by commas, trimming whitespace on each token */
550 while ((token = strsep(&s, ",")) != NULL) {
551 /* trim leading whitespace */
552 while (*token && isspace((unsigned char)*token)) {
553 token++;
554 }
555
556 /* trim trailing whitespace */
557 char *end = token + strlen(token);
558 while (end > token && isspace((unsigned char)end[-1])) {
559 *--end = '\0';
560 }
561
562 if (*token) {
563 /* Expand the items array and append the token */
564 char **arr = (char **)realloc(result.items, (result.count + 1) * sizeof(char *));
565 if (!arr) {
566 /* Allocation failed, stop and return what we have */
567 break;
568 }
569 result.items = arr;
570 result.items[result.count++] = strdup(token);
571 }
572 }
573
574 free(tmp);
575 return result;
576}
577
578/**
579 * flag_list_contains - Case-insensitive membership test in a flag_list.
580 *
581 * Input:
582 * list - pointer to a flag_list
583 * needle - NUL terminated string to search for
584 *
585 * Returns:
586 * true if needle is contained in list (strcasecmp)
587 * false otherwise
588 */
589static bool flag_list_contains(const flag_list *list, const char *needle) {
590 if (!list || !needle || !*needle) {
591 return false;
592 }
593
594 for (size_t i = 0; i < list->count; i++) {
595 if (strcasecmp(list->items[i], needle) == 0) {
596 return true;
597 }
598 }
599 return false;
600}
601
602/**
603 * free_flag_list - Release all heap allocations held by a flag_list.
604 *
605 * Input:
606 * list - pointer to a flag_list whose items were allocated by
607 * parse_flags_line() or split_csv_trim().
608 *
609 * After this call list->items is NULL and list->count is 0.
610 */
611static void free_flag_list(flag_list *list) {
612 if (!list || !list->items) {
613 return;
614 }
615
616 for (size_t i = 0; i < list->count; i++) {
617 free(list->items[i]);
618 }
619 free(list->items);
620
621 list->items = NULL;
622 list->count = 0;
352} 623}