1// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
2/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
3#define _GNU_SOURCE
4#include <argp.h>
5#include <string.h>
6#include <stdlib.h>
7#include <sched.h>
8#include <pthread.h>
9#include <dirent.h>
10#include <signal.h>
11#include <fcntl.h>
12#include <unistd.h>
13#include <sys/time.h>
14#include <sys/sysinfo.h>
15#include <sys/stat.h>
16#include <bpf/libbpf.h>
17#include <bpf/btf.h>
18#include <libelf.h>
19#include <gelf.h>
20#include <float.h>
21#include <math.h>
22
23#ifndef ARRAY_SIZE
24#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
25#endif
26
27enum stat_id {
28	VERDICT,
29	DURATION,
30	TOTAL_INSNS,
31	TOTAL_STATES,
32	PEAK_STATES,
33	MAX_STATES_PER_INSN,
34	MARK_READ_MAX_LEN,
35
36	FILE_NAME,
37	PROG_NAME,
38
39	ALL_STATS_CNT,
40	NUM_STATS_CNT = FILE_NAME - VERDICT,
41};
42
43/* In comparison mode each stat can specify up to four different values:
44 *   - A side value;
45 *   - B side value;
46 *   - absolute diff value;
47 *   - relative (percentage) diff value.
48 *
49 * When specifying stat specs in comparison mode, user can use one of the
50 * following variant suffixes to specify which exact variant should be used for
51 * ordering or filtering:
52 *   - `_a` for A side value;
53 *   - `_b` for B side value;
54 *   - `_diff` for absolute diff value;
55 *   - `_pct` for relative (percentage) diff value.
56 *
57 * If no variant suffix is provided, then `_b` (control data) is assumed.
58 *
59 * As an example, let's say instructions stat has the following output:
60 *
61 * Insns (A)  Insns (B)  Insns   (DIFF)
62 * ---------  ---------  --------------
63 * 21547      20920       -627 (-2.91%)
64 *
65 * Then:
66 *   - 21547 is A side value (insns_a);
67 *   - 20920 is B side value (insns_b);
68 *   - -627 is absolute diff value (insns_diff);
69 *   - -2.91% is relative diff value (insns_pct).
70 *
71 * For verdict there is no verdict_pct variant.
72 * For file and program name, _a and _b variants are equivalent and there are
73 * no _diff or _pct variants.
74 */
75enum stat_variant {
76	VARIANT_A,
77	VARIANT_B,
78	VARIANT_DIFF,
79	VARIANT_PCT,
80};
81
82struct verif_stats {
83	char *file_name;
84	char *prog_name;
85
86	long stats[NUM_STATS_CNT];
87};
88
89/* joined comparison mode stats */
90struct verif_stats_join {
91	char *file_name;
92	char *prog_name;
93
94	const struct verif_stats *stats_a;
95	const struct verif_stats *stats_b;
96};
97
98struct stat_specs {
99	int spec_cnt;
100	enum stat_id ids[ALL_STATS_CNT];
101	enum stat_variant variants[ALL_STATS_CNT];
102	bool asc[ALL_STATS_CNT];
103	bool abs[ALL_STATS_CNT];
104	int lens[ALL_STATS_CNT * 3]; /* 3x for comparison mode */
105};
106
107enum resfmt {
108	RESFMT_TABLE,
109	RESFMT_TABLE_CALCLEN, /* fake format to pre-calculate table's column widths */
110	RESFMT_CSV,
111};
112
113enum filter_kind {
114	FILTER_NAME,
115	FILTER_STAT,
116};
117
118enum operator_kind {
119	OP_EQ,		/* == or = */
120	OP_NEQ,		/* != or <> */
121	OP_LT,		/* < */
122	OP_LE,		/* <= */
123	OP_GT,		/* > */
124	OP_GE,		/* >= */
125};
126
127struct filter {
128	enum filter_kind kind;
129	/* FILTER_NAME */
130	char *any_glob;
131	char *file_glob;
132	char *prog_glob;
133	/* FILTER_STAT */
134	enum operator_kind op;
135	int stat_id;
136	enum stat_variant stat_var;
137	long value;
138	bool abs;
139};
140
141static struct env {
142	char **filenames;
143	int filename_cnt;
144	bool verbose;
145	bool debug;
146	bool quiet;
147	bool force_checkpoints;
148	bool force_reg_invariants;
149	enum resfmt out_fmt;
150	bool show_version;
151	bool comparison_mode;
152	bool replay_mode;
153	int top_n;
154
155	int log_level;
156	int log_size;
157	bool log_fixed;
158
159	struct verif_stats *prog_stats;
160	int prog_stat_cnt;
161
162	/* baseline_stats is allocated and used only in comparison mode */
163	struct verif_stats *baseline_stats;
164	int baseline_stat_cnt;
165
166	struct verif_stats_join *join_stats;
167	int join_stat_cnt;
168
169	struct stat_specs output_spec;
170	struct stat_specs sort_spec;
171
172	struct filter *allow_filters;
173	struct filter *deny_filters;
174	int allow_filter_cnt;
175	int deny_filter_cnt;
176
177	int files_processed;
178	int files_skipped;
179	int progs_processed;
180	int progs_skipped;
181} env;
182
183static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
184{
185	if (!env.verbose)
186		return 0;
187	if (level == LIBBPF_DEBUG  && !env.debug)
188		return 0;
189	return vfprintf(stderr, format, args);
190}
191
192#ifndef VERISTAT_VERSION
193#define VERISTAT_VERSION "<kernel>"
194#endif
195
196const char *argp_program_version = "veristat v" VERISTAT_VERSION;
197const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
198const char argp_program_doc[] =
199"veristat    BPF verifier stats collection and comparison tool.\n"
200"\n"
201"USAGE: veristat <obj-file> [<obj-file>...]\n"
202"   OR: veristat -C <baseline.csv> <comparison.csv>\n"
203"   OR: veristat -R <results.csv>\n";
204
205enum {
206	OPT_LOG_FIXED = 1000,
207	OPT_LOG_SIZE = 1001,
208};
209
210static const struct argp_option opts[] = {
211	{ NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help" },
212	{ "version", 'V', NULL, 0, "Print version" },
213	{ "verbose", 'v', NULL, 0, "Verbose mode" },
214	{ "debug", 'd', NULL, 0, "Debug mode (turns on libbpf debug logging)" },
215	{ "log-level", 'l', "LEVEL", 0, "Verifier log level (default 0 for normal mode, 1 for verbose mode)" },
216	{ "log-fixed", OPT_LOG_FIXED, NULL, 0, "Disable verifier log rotation" },
217	{ "log-size", OPT_LOG_SIZE, "BYTES", 0, "Customize verifier log size (default to 16MB)" },
218	{ "top-n", 'n', "N", 0, "Emit only up to first N results." },
219	{ "quiet", 'q', NULL, 0, "Quiet mode" },
220	{ "emit", 'e', "SPEC", 0, "Specify stats to be emitted" },
221	{ "sort", 's', "SPEC", 0, "Specify sort order" },
222	{ "output-format", 'o', "FMT", 0, "Result output format (table, csv), default is table." },
223	{ "compare", 'C', NULL, 0, "Comparison mode" },
224	{ "replay", 'R', NULL, 0, "Replay mode" },
225	{ "filter", 'f', "FILTER", 0, "Filter expressions (or @filename for file with expressions)." },
226	{ "test-states", 't', NULL, 0,
227	  "Force frequent BPF verifier state checkpointing (set BPF_F_TEST_STATE_FREQ program flag)" },
228	{ "test-reg-invariants", 'r', NULL, 0,
229	  "Force BPF verifier failure on register invariant violation (BPF_F_TEST_REG_INVARIANTS program flag)" },
230	{},
231};
232
233static int parse_stats(const char *stats_str, struct stat_specs *specs);
234static int append_filter(struct filter **filters, int *cnt, const char *str);
235static int append_filter_file(const char *path);
236
237static error_t parse_arg(int key, char *arg, struct argp_state *state)
238{
239	void *tmp;
240	int err;
241
242	switch (key) {
243	case 'h':
244		argp_state_help(state, stderr, ARGP_HELP_STD_HELP);
245		break;
246	case 'V':
247		env.show_version = true;
248		break;
249	case 'v':
250		env.verbose = true;
251		break;
252	case 'd':
253		env.debug = true;
254		env.verbose = true;
255		break;
256	case 'q':
257		env.quiet = true;
258		break;
259	case 'e':
260		err = parse_stats(arg, &env.output_spec);
261		if (err)
262			return err;
263		break;
264	case 's':
265		err = parse_stats(arg, &env.sort_spec);
266		if (err)
267			return err;
268		break;
269	case 'o':
270		if (strcmp(arg, "table") == 0) {
271			env.out_fmt = RESFMT_TABLE;
272		} else if (strcmp(arg, "csv") == 0) {
273			env.out_fmt = RESFMT_CSV;
274		} else {
275			fprintf(stderr, "Unrecognized output format '%s'\n", arg);
276			return -EINVAL;
277		}
278		break;
279	case 'l':
280		errno = 0;
281		env.log_level = strtol(arg, NULL, 10);
282		if (errno) {
283			fprintf(stderr, "invalid log level: %s\n", arg);
284			argp_usage(state);
285		}
286		break;
287	case OPT_LOG_FIXED:
288		env.log_fixed = true;
289		break;
290	case OPT_LOG_SIZE:
291		errno = 0;
292		env.log_size = strtol(arg, NULL, 10);
293		if (errno) {
294			fprintf(stderr, "invalid log size: %s\n", arg);
295			argp_usage(state);
296		}
297		break;
298	case 't':
299		env.force_checkpoints = true;
300		break;
301	case 'r':
302		env.force_reg_invariants = true;
303		break;
304	case 'n':
305		errno = 0;
306		env.top_n = strtol(arg, NULL, 10);
307		if (errno) {
308			fprintf(stderr, "invalid top N specifier: %s\n", arg);
309			argp_usage(state);
310		}
311	case 'C':
312		env.comparison_mode = true;
313		break;
314	case 'R':
315		env.replay_mode = true;
316		break;
317	case 'f':
318		if (arg[0] == '@')
319			err = append_filter_file(arg + 1);
320		else if (arg[0] == '!')
321			err = append_filter(&env.deny_filters, &env.deny_filter_cnt, arg + 1);
322		else
323			err = append_filter(&env.allow_filters, &env.allow_filter_cnt, arg);
324		if (err) {
325			fprintf(stderr, "Failed to collect program filter expressions: %d\n", err);
326			return err;
327		}
328		break;
329	case ARGP_KEY_ARG:
330		tmp = realloc(env.filenames, (env.filename_cnt + 1) * sizeof(*env.filenames));
331		if (!tmp)
332			return -ENOMEM;
333		env.filenames = tmp;
334		env.filenames[env.filename_cnt] = strdup(arg);
335		if (!env.filenames[env.filename_cnt])
336			return -ENOMEM;
337		env.filename_cnt++;
338		break;
339	default:
340		return ARGP_ERR_UNKNOWN;
341	}
342	return 0;
343}
344
345static const struct argp argp = {
346	.options = opts,
347	.parser = parse_arg,
348	.doc = argp_program_doc,
349};
350
351
352/* Adapted from perf/util/string.c */
353static bool glob_matches(const char *str, const char *pat)
354{
355	while (*str && *pat && *pat != '*') {
356		if (*str != *pat)
357			return false;
358		str++;
359		pat++;
360	}
361	/* Check wild card */
362	if (*pat == '*') {
363		while (*pat == '*')
364			pat++;
365		if (!*pat) /* Tail wild card matches all */
366			return true;
367		while (*str)
368			if (glob_matches(str++, pat))
369				return true;
370	}
371	return !*str && !*pat;
372}
373
374static bool is_bpf_obj_file(const char *path) {
375	Elf64_Ehdr *ehdr;
376	int fd, err = -EINVAL;
377	Elf *elf = NULL;
378
379	fd = open(path, O_RDONLY | O_CLOEXEC);
380	if (fd < 0)
381		return true; /* we'll fail later and propagate error */
382
383	/* ensure libelf is initialized */
384	(void)elf_version(EV_CURRENT);
385
386	elf = elf_begin(fd, ELF_C_READ, NULL);
387	if (!elf)
388		goto cleanup;
389
390	if (elf_kind(elf) != ELF_K_ELF || gelf_getclass(elf) != ELFCLASS64)
391		goto cleanup;
392
393	ehdr = elf64_getehdr(elf);
394	/* Old LLVM set e_machine to EM_NONE */
395	if (!ehdr || ehdr->e_type != ET_REL || (ehdr->e_machine && ehdr->e_machine != EM_BPF))
396		goto cleanup;
397
398	err = 0;
399cleanup:
400	if (elf)
401		elf_end(elf);
402	close(fd);
403	return err == 0;
404}
405
406static bool should_process_file_prog(const char *filename, const char *prog_name)
407{
408	struct filter *f;
409	int i, allow_cnt = 0;
410
411	for (i = 0; i < env.deny_filter_cnt; i++) {
412		f = &env.deny_filters[i];
413		if (f->kind != FILTER_NAME)
414			continue;
415
416		if (f->any_glob && glob_matches(filename, f->any_glob))
417			return false;
418		if (f->any_glob && prog_name && glob_matches(prog_name, f->any_glob))
419			return false;
420		if (f->file_glob && glob_matches(filename, f->file_glob))
421			return false;
422		if (f->prog_glob && prog_name && glob_matches(prog_name, f->prog_glob))
423			return false;
424	}
425
426	for (i = 0; i < env.allow_filter_cnt; i++) {
427		f = &env.allow_filters[i];
428		if (f->kind != FILTER_NAME)
429			continue;
430
431		allow_cnt++;
432		if (f->any_glob) {
433			if (glob_matches(filename, f->any_glob))
434				return true;
435			/* If we don't know program name yet, any_glob filter
436			 * has to assume that current BPF object file might be
437			 * relevant; we'll check again later on after opening
438			 * BPF object file, at which point program name will
439			 * be known finally.
440			 */
441			if (!prog_name || glob_matches(prog_name, f->any_glob))
442				return true;
443		} else {
444			if (f->file_glob && !glob_matches(filename, f->file_glob))
445				continue;
446			if (f->prog_glob && prog_name && !glob_matches(prog_name, f->prog_glob))
447				continue;
448			return true;
449		}
450	}
451
452	/* if there are no file/prog name allow filters, allow all progs,
453	 * unless they are denied earlier explicitly
454	 */
455	return allow_cnt == 0;
456}
457
458static struct {
459	enum operator_kind op_kind;
460	const char *op_str;
461} operators[] = {
462	/* Order of these definitions matter to avoid situations like '<'
463	 * matching part of what is actually a '<>' operator. That is,
464	 * substrings should go last.
465	 */
466	{ OP_EQ, "==" },
467	{ OP_NEQ, "!=" },
468	{ OP_NEQ, "<>" },
469	{ OP_LE, "<=" },
470	{ OP_LT, "<" },
471	{ OP_GE, ">=" },
472	{ OP_GT, ">" },
473	{ OP_EQ, "=" },
474};
475
476static bool parse_stat_id_var(const char *name, size_t len, int *id,
477			      enum stat_variant *var, bool *is_abs);
478
479static int append_filter(struct filter **filters, int *cnt, const char *str)
480{
481	struct filter *f;
482	void *tmp;
483	const char *p;
484	int i;
485
486	tmp = realloc(*filters, (*cnt + 1) * sizeof(**filters));
487	if (!tmp)
488		return -ENOMEM;
489	*filters = tmp;
490
491	f = &(*filters)[*cnt];
492	memset(f, 0, sizeof(*f));
493
494	/* First, let's check if it's a stats filter of the following form:
495	 * <stat><op><value, where:
496	 *   - <stat> is one of supported numerical stats (verdict is also
497	 *     considered numerical, failure == 0, success == 1);
498	 *   - <op> is comparison operator (see `operators` definitions);
499	 *   - <value> is an integer (or failure/success, or false/true as
500	 *     special aliases for 0 and 1, respectively).
501	 * If the form doesn't match what user provided, we assume file/prog
502	 * glob filter.
503	 */
504	for (i = 0; i < ARRAY_SIZE(operators); i++) {
505		enum stat_variant var;
506		int id;
507		long val;
508		const char *end = str;
509		const char *op_str;
510		bool is_abs;
511
512		op_str = operators[i].op_str;
513		p = strstr(str, op_str);
514		if (!p)
515			continue;
516
517		if (!parse_stat_id_var(str, p - str, &id, &var, &is_abs)) {
518			fprintf(stderr, "Unrecognized stat name in '%s'!\n", str);
519			return -EINVAL;
520		}
521		if (id >= FILE_NAME) {
522			fprintf(stderr, "Non-integer stat is specified in '%s'!\n", str);
523			return -EINVAL;
524		}
525
526		p += strlen(op_str);
527
528		if (strcasecmp(p, "true") == 0 ||
529		    strcasecmp(p, "t") == 0 ||
530		    strcasecmp(p, "success") == 0 ||
531		    strcasecmp(p, "succ") == 0 ||
532		    strcasecmp(p, "s") == 0 ||
533		    strcasecmp(p, "match") == 0 ||
534		    strcasecmp(p, "m") == 0) {
535			val = 1;
536		} else if (strcasecmp(p, "false") == 0 ||
537			   strcasecmp(p, "f") == 0 ||
538			   strcasecmp(p, "failure") == 0 ||
539			   strcasecmp(p, "fail") == 0 ||
540			   strcasecmp(p, "mismatch") == 0 ||
541			   strcasecmp(p, "mis") == 0) {
542			val = 0;
543		} else {
544			errno = 0;
545			val = strtol(p, (char **)&end, 10);
546			if (errno || end == p || *end != '\0' ) {
547				fprintf(stderr, "Invalid integer value in '%s'!\n", str);
548				return -EINVAL;
549			}
550		}
551
552		f->kind = FILTER_STAT;
553		f->stat_id = id;
554		f->stat_var = var;
555		f->op = operators[i].op_kind;
556		f->abs = true;
557		f->value = val;
558
559		*cnt += 1;
560		return 0;
561	}
562
563	/* File/prog filter can be specified either as '<glob>' or
564	 * '<file-glob>/<prog-glob>'. In the former case <glob> is applied to
565	 * both file and program names. This seems to be way more useful in
566	 * practice. If user needs full control, they can use '/<prog-glob>'
567	 * form to glob just program name, or '<file-glob>/' to glob only file
568	 * name. But usually common <glob> seems to be the most useful and
569	 * ergonomic way.
570	 */
571	f->kind = FILTER_NAME;
572	p = strchr(str, '/');
573	if (!p) {
574		f->any_glob = strdup(str);
575		if (!f->any_glob)
576			return -ENOMEM;
577	} else {
578		if (str != p) {
579			/* non-empty file glob */
580			f->file_glob = strndup(str, p - str);
581			if (!f->file_glob)
582				return -ENOMEM;
583		}
584		if (strlen(p + 1) > 0) {
585			/* non-empty prog glob */
586			f->prog_glob = strdup(p + 1);
587			if (!f->prog_glob) {
588				free(f->file_glob);
589				f->file_glob = NULL;
590				return -ENOMEM;
591			}
592		}
593	}
594
595	*cnt += 1;
596	return 0;
597}
598
599static int append_filter_file(const char *path)
600{
601	char buf[1024];
602	FILE *f;
603	int err = 0;
604
605	f = fopen(path, "r");
606	if (!f) {
607		err = -errno;
608		fprintf(stderr, "Failed to open filters in '%s': %d\n", path, err);
609		return err;
610	}
611
612	while (fscanf(f, " %1023[^\n]\n", buf) == 1) {
613		/* lines starting with # are comments, skip them */
614		if (buf[0] == '\0' || buf[0] == '#')
615			continue;
616		/* lines starting with ! are negative match filters */
617		if (buf[0] == '!')
618			err = append_filter(&env.deny_filters, &env.deny_filter_cnt, buf + 1);
619		else
620			err = append_filter(&env.allow_filters, &env.allow_filter_cnt, buf);
621		if (err)
622			goto cleanup;
623	}
624
625cleanup:
626	fclose(f);
627	return err;
628}
629
630static const struct stat_specs default_output_spec = {
631	.spec_cnt = 7,
632	.ids = {
633		FILE_NAME, PROG_NAME, VERDICT, DURATION,
634		TOTAL_INSNS, TOTAL_STATES, PEAK_STATES,
635	},
636};
637
638static const struct stat_specs default_csv_output_spec = {
639	.spec_cnt = 9,
640	.ids = {
641		FILE_NAME, PROG_NAME, VERDICT, DURATION,
642		TOTAL_INSNS, TOTAL_STATES, PEAK_STATES,
643		MAX_STATES_PER_INSN, MARK_READ_MAX_LEN,
644	},
645};
646
647static const struct stat_specs default_sort_spec = {
648	.spec_cnt = 2,
649	.ids = {
650		FILE_NAME, PROG_NAME,
651	},
652	.asc = { true, true, },
653};
654
655/* sorting for comparison mode to join two data sets */
656static const struct stat_specs join_sort_spec = {
657	.spec_cnt = 2,
658	.ids = {
659		FILE_NAME, PROG_NAME,
660	},
661	.asc = { true, true, },
662};
663
664static struct stat_def {
665	const char *header;
666	const char *names[4];
667	bool asc_by_default;
668	bool left_aligned;
669} stat_defs[] = {
670	[FILE_NAME] = { "File", {"file_name", "filename", "file"}, true /* asc */, true /* left */ },
671	[PROG_NAME] = { "Program", {"prog_name", "progname", "prog"}, true /* asc */, true /* left */ },
672	[VERDICT] = { "Verdict", {"verdict"}, true /* asc: failure, success */, true /* left */ },
673	[DURATION] = { "Duration (us)", {"duration", "dur"}, },
674	[TOTAL_INSNS] = { "Insns", {"total_insns", "insns"}, },
675	[TOTAL_STATES] = { "States", {"total_states", "states"}, },
676	[PEAK_STATES] = { "Peak states", {"peak_states"}, },
677	[MAX_STATES_PER_INSN] = { "Max states per insn", {"max_states_per_insn"}, },
678	[MARK_READ_MAX_LEN] = { "Max mark read length", {"max_mark_read_len", "mark_read"}, },
679};
680
681static bool parse_stat_id_var(const char *name, size_t len, int *id,
682			      enum stat_variant *var, bool *is_abs)
683{
684	static const char *var_sfxs[] = {
685		[VARIANT_A] = "_a",
686		[VARIANT_B] = "_b",
687		[VARIANT_DIFF] = "_diff",
688		[VARIANT_PCT] = "_pct",
689	};
690	int i, j, k;
691
692	/* |<stat>| means we take absolute value of given stat */
693	*is_abs = false;
694	if (len > 2 && name[0] == '|' && name[len - 1] == '|') {
695		*is_abs = true;
696		name += 1;
697		len -= 2;
698	}
699
700	for (i = 0; i < ARRAY_SIZE(stat_defs); i++) {
701		struct stat_def *def = &stat_defs[i];
702		size_t alias_len, sfx_len;
703		const char *alias;
704
705		for (j = 0; j < ARRAY_SIZE(stat_defs[i].names); j++) {
706			alias = def->names[j];
707			if (!alias)
708				continue;
709
710			alias_len = strlen(alias);
711			if (strncmp(name, alias, alias_len) != 0)
712				continue;
713
714			if (alias_len == len) {
715				/* If no variant suffix is specified, we
716				 * assume control group (just in case we are
717				 * in comparison mode. Variant is ignored in
718				 * non-comparison mode.
719				 */
720				*var = VARIANT_B;
721				*id = i;
722				return true;
723			}
724
725			for (k = 0; k < ARRAY_SIZE(var_sfxs); k++) {
726				sfx_len = strlen(var_sfxs[k]);
727				if (alias_len + sfx_len != len)
728					continue;
729
730				if (strncmp(name + alias_len, var_sfxs[k], sfx_len) == 0) {
731					*var = (enum stat_variant)k;
732					*id = i;
733					return true;
734				}
735			}
736		}
737	}
738
739	return false;
740}
741
742static bool is_asc_sym(char c)
743{
744	return c == '^';
745}
746
747static bool is_desc_sym(char c)
748{
749	return c == 'v' || c == 'V' || c == '.' || c == '!' || c == '_';
750}
751
752static int parse_stat(const char *stat_name, struct stat_specs *specs)
753{
754	int id;
755	bool has_order = false, is_asc = false, is_abs = false;
756	size_t len = strlen(stat_name);
757	enum stat_variant var;
758
759	if (specs->spec_cnt >= ARRAY_SIZE(specs->ids)) {
760		fprintf(stderr, "Can't specify more than %zd stats\n", ARRAY_SIZE(specs->ids));
761		return -E2BIG;
762	}
763
764	if (len > 1 && (is_asc_sym(stat_name[len - 1]) || is_desc_sym(stat_name[len - 1]))) {
765		has_order = true;
766		is_asc = is_asc_sym(stat_name[len - 1]);
767		len -= 1;
768	}
769
770	if (!parse_stat_id_var(stat_name, len, &id, &var, &is_abs)) {
771		fprintf(stderr, "Unrecognized stat name '%s'\n", stat_name);
772		return -ESRCH;
773	}
774
775	specs->ids[specs->spec_cnt] = id;
776	specs->variants[specs->spec_cnt] = var;
777	specs->asc[specs->spec_cnt] = has_order ? is_asc : stat_defs[id].asc_by_default;
778	specs->abs[specs->spec_cnt] = is_abs;
779	specs->spec_cnt++;
780
781	return 0;
782}
783
784static int parse_stats(const char *stats_str, struct stat_specs *specs)
785{
786	char *input, *state = NULL, *next;
787	int err;
788
789	input = strdup(stats_str);
790	if (!input)
791		return -ENOMEM;
792
793	while ((next = strtok_r(state ? NULL : input, ",", &state))) {
794		err = parse_stat(next, specs);
795		if (err)
796			return err;
797	}
798
799	return 0;
800}
801
802static void free_verif_stats(struct verif_stats *stats, size_t stat_cnt)
803{
804	int i;
805
806	if (!stats)
807		return;
808
809	for (i = 0; i < stat_cnt; i++) {
810		free(stats[i].file_name);
811		free(stats[i].prog_name);
812	}
813	free(stats);
814}
815
816static char verif_log_buf[64 * 1024];
817
818#define MAX_PARSED_LOG_LINES 100
819
820static int parse_verif_log(char * const buf, size_t buf_sz, struct verif_stats *s)
821{
822	const char *cur;
823	int pos, lines;
824
825	buf[buf_sz - 1] = '\0';
826
827	for (pos = strlen(buf) - 1, lines = 0; pos >= 0 && lines < MAX_PARSED_LOG_LINES; lines++) {
828		/* find previous endline or otherwise take the start of log buf */
829		for (cur = &buf[pos]; cur > buf && cur[0] != '\n'; cur--, pos--) {
830		}
831		/* next time start from end of previous line (or pos goes to <0) */
832		pos--;
833		/* if we found endline, point right after endline symbol;
834		 * otherwise, stay at the beginning of log buf
835		 */
836		if (cur[0] == '\n')
837			cur++;
838
839		if (1 == sscanf(cur, "verification time %ld usec\n", &s->stats[DURATION]))
840			continue;
841		if (6 == sscanf(cur, "processed %ld insns (limit %*d) max_states_per_insn %ld total_states %ld peak_states %ld mark_read %ld",
842				&s->stats[TOTAL_INSNS],
843				&s->stats[MAX_STATES_PER_INSN],
844				&s->stats[TOTAL_STATES],
845				&s->stats[PEAK_STATES],
846				&s->stats[MARK_READ_MAX_LEN]))
847			continue;
848	}
849
850	return 0;
851}
852
853static int guess_prog_type_by_ctx_name(const char *ctx_name,
854				       enum bpf_prog_type *prog_type,
855				       enum bpf_attach_type *attach_type)
856{
857	/* We need to guess program type based on its declared context type.
858	 * This guess can't be perfect as many different program types might
859	 * share the same context type.  So we can only hope to reasonably
860	 * well guess this and get lucky.
861	 *
862	 * Just in case, we support both UAPI-side type names and
863	 * kernel-internal names.
864	 */
865	static struct {
866		const char *uapi_name;
867		const char *kern_name;
868		enum bpf_prog_type prog_type;
869		enum bpf_attach_type attach_type;
870	} ctx_map[] = {
871		/* __sk_buff is most ambiguous, we assume TC program */
872		{ "__sk_buff", "sk_buff", BPF_PROG_TYPE_SCHED_CLS },
873		{ "bpf_sock", "sock", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND },
874		{ "bpf_sock_addr", "bpf_sock_addr_kern",  BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND },
875		{ "bpf_sock_ops", "bpf_sock_ops_kern", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS },
876		{ "sk_msg_md", "sk_msg", BPF_PROG_TYPE_SK_MSG, BPF_SK_MSG_VERDICT },
877		{ "bpf_cgroup_dev_ctx", "bpf_cgroup_dev_ctx", BPF_PROG_TYPE_CGROUP_DEVICE, BPF_CGROUP_DEVICE },
878		{ "bpf_sysctl", "bpf_sysctl_kern", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL },
879		{ "bpf_sockopt", "bpf_sockopt_kern", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT },
880		{ "sk_reuseport_md", "sk_reuseport_kern", BPF_PROG_TYPE_SK_REUSEPORT, BPF_SK_REUSEPORT_SELECT_OR_MIGRATE },
881		{ "bpf_sk_lookup", "bpf_sk_lookup_kern", BPF_PROG_TYPE_SK_LOOKUP, BPF_SK_LOOKUP },
882		{ "xdp_md", "xdp_buff", BPF_PROG_TYPE_XDP, BPF_XDP },
883		/* tracing types with no expected attach type */
884		{ "bpf_user_pt_regs_t", "pt_regs", BPF_PROG_TYPE_KPROBE },
885		{ "bpf_perf_event_data", "bpf_perf_event_data_kern", BPF_PROG_TYPE_PERF_EVENT },
886		/* raw_tp programs use u64[] from kernel side, we don't want
887		 * to match on that, probably; so NULL for kern-side type
888		 */
889		{ "bpf_raw_tracepoint_args", NULL, BPF_PROG_TYPE_RAW_TRACEPOINT },
890	};
891	int i;
892
893	if (!ctx_name)
894		return -EINVAL;
895
896	for (i = 0; i < ARRAY_SIZE(ctx_map); i++) {
897		if (strcmp(ctx_map[i].uapi_name, ctx_name) == 0 ||
898		    (ctx_map[i].kern_name && strcmp(ctx_map[i].kern_name, ctx_name) == 0)) {
899			*prog_type = ctx_map[i].prog_type;
900			*attach_type = ctx_map[i].attach_type;
901			return 0;
902		}
903	}
904
905	return -ESRCH;
906}
907
908static void fixup_obj(struct bpf_object *obj, struct bpf_program *prog, const char *filename)
909{
910	struct bpf_map *map;
911
912	bpf_object__for_each_map(map, obj) {
913		/* disable pinning */
914		bpf_map__set_pin_path(map, NULL);
915
916		/* fix up map size, if necessary */
917		switch (bpf_map__type(map)) {
918		case BPF_MAP_TYPE_SK_STORAGE:
919		case BPF_MAP_TYPE_TASK_STORAGE:
920		case BPF_MAP_TYPE_INODE_STORAGE:
921		case BPF_MAP_TYPE_CGROUP_STORAGE:
922			break;
923		default:
924			if (bpf_map__max_entries(map) == 0)
925				bpf_map__set_max_entries(map, 1);
926		}
927	}
928
929	/* SEC(freplace) programs can't be loaded with veristat as is,
930	 * but we can try guessing their target program's expected type by
931	 * looking at the type of program's first argument and substituting
932	 * corresponding program type
933	 */
934	if (bpf_program__type(prog) == BPF_PROG_TYPE_EXT) {
935		const struct btf *btf = bpf_object__btf(obj);
936		const char *prog_name = bpf_program__name(prog);
937		enum bpf_prog_type prog_type;
938		enum bpf_attach_type attach_type;
939		const struct btf_type *t;
940		const char *ctx_name;
941		int id;
942
943		if (!btf)
944			goto skip_freplace_fixup;
945
946		id = btf__find_by_name_kind(btf, prog_name, BTF_KIND_FUNC);
947		t = btf__type_by_id(btf, id);
948		t = btf__type_by_id(btf, t->type);
949		if (!btf_is_func_proto(t) || btf_vlen(t) != 1)
950			goto skip_freplace_fixup;
951
952		/* context argument is a pointer to a struct/typedef */
953		t = btf__type_by_id(btf, btf_params(t)[0].type);
954		while (t && btf_is_mod(t))
955			t = btf__type_by_id(btf, t->type);
956		if (!t || !btf_is_ptr(t))
957			goto skip_freplace_fixup;
958		t = btf__type_by_id(btf, t->type);
959		while (t && btf_is_mod(t))
960			t = btf__type_by_id(btf, t->type);
961		if (!t)
962			goto skip_freplace_fixup;
963
964		ctx_name = btf__name_by_offset(btf, t->name_off);
965
966		if (guess_prog_type_by_ctx_name(ctx_name, &prog_type, &attach_type) == 0) {
967			bpf_program__set_type(prog, prog_type);
968			bpf_program__set_expected_attach_type(prog, attach_type);
969
970			if (!env.quiet) {
971				printf("Using guessed program type '%s' for %s/%s...\n",
972					libbpf_bpf_prog_type_str(prog_type),
973					filename, prog_name);
974			}
975		} else {
976			if (!env.quiet) {
977				printf("Failed to guess program type for freplace program with context type name '%s' for %s/%s. Consider using canonical type names to help veristat...\n",
978					ctx_name, filename, prog_name);
979			}
980		}
981	}
982skip_freplace_fixup:
983	return;
984}
985
986static int process_prog(const char *filename, struct bpf_object *obj, struct bpf_program *prog)
987{
988	const char *prog_name = bpf_program__name(prog);
989	const char *base_filename = basename(filename);
990	char *buf;
991	int buf_sz, log_level;
992	struct verif_stats *stats;
993	int err = 0;
994	void *tmp;
995
996	if (!should_process_file_prog(base_filename, bpf_program__name(prog))) {
997		env.progs_skipped++;
998		return 0;
999	}
1000
1001	tmp = realloc(env.prog_stats, (env.prog_stat_cnt + 1) * sizeof(*env.prog_stats));
1002	if (!tmp)
1003		return -ENOMEM;
1004	env.prog_stats = tmp;
1005	stats = &env.prog_stats[env.prog_stat_cnt++];
1006	memset(stats, 0, sizeof(*stats));
1007
1008	if (env.verbose) {
1009		buf_sz = env.log_size ? env.log_size : 16 * 1024 * 1024;
1010		buf = malloc(buf_sz);
1011		if (!buf)
1012			return -ENOMEM;
1013		/* ensure we always request stats */
1014		log_level = env.log_level | 4 | (env.log_fixed ? 8 : 0);
1015	} else {
1016		buf = verif_log_buf;
1017		buf_sz = sizeof(verif_log_buf);
1018		/* request only verifier stats */
1019		log_level = 4 | (env.log_fixed ? 8 : 0);
1020	}
1021	verif_log_buf[0] = '\0';
1022
1023	bpf_program__set_log_buf(prog, buf, buf_sz);
1024	bpf_program__set_log_level(prog, log_level);
1025
1026	/* increase chances of successful BPF object loading */
1027	fixup_obj(obj, prog, base_filename);
1028
1029	if (env.force_checkpoints)
1030		bpf_program__set_flags(prog, bpf_program__flags(prog) | BPF_F_TEST_STATE_FREQ);
1031	if (env.force_reg_invariants)
1032		bpf_program__set_flags(prog, bpf_program__flags(prog) | BPF_F_TEST_REG_INVARIANTS);
1033
1034	err = bpf_object__load(obj);
1035	env.progs_processed++;
1036
1037	stats->file_name = strdup(base_filename);
1038	stats->prog_name = strdup(bpf_program__name(prog));
1039	stats->stats[VERDICT] = err == 0; /* 1 - success, 0 - failure */
1040	parse_verif_log(buf, buf_sz, stats);
1041
1042	if (env.verbose) {
1043		printf("PROCESSING %s/%s, DURATION US: %ld, VERDICT: %s, VERIFIER LOG:\n%s\n",
1044		       filename, prog_name, stats->stats[DURATION],
1045		       err ? "failure" : "success", buf);
1046	}
1047
1048	if (verif_log_buf != buf)
1049		free(buf);
1050
1051	return 0;
1052};
1053
1054static int process_obj(const char *filename)
1055{
1056	struct bpf_object *obj = NULL, *tobj;
1057	struct bpf_program *prog, *tprog, *lprog;
1058	libbpf_print_fn_t old_libbpf_print_fn;
1059	LIBBPF_OPTS(bpf_object_open_opts, opts);
1060	int err = 0, prog_cnt = 0;
1061
1062	if (!should_process_file_prog(basename(filename), NULL)) {
1063		if (env.verbose)
1064			printf("Skipping '%s' due to filters...\n", filename);
1065		env.files_skipped++;
1066		return 0;
1067	}
1068	if (!is_bpf_obj_file(filename)) {
1069		if (env.verbose)
1070			printf("Skipping '%s' as it's not a BPF object file...\n", filename);
1071		env.files_skipped++;
1072		return 0;
1073	}
1074
1075	if (!env.quiet && env.out_fmt == RESFMT_TABLE)
1076		printf("Processing '%s'...\n", basename(filename));
1077
1078	old_libbpf_print_fn = libbpf_set_print(libbpf_print_fn);
1079	obj = bpf_object__open_file(filename, &opts);
1080	if (!obj) {
1081		/* if libbpf can't open BPF object file, it could be because
1082		 * that BPF object file is incomplete and has to be statically
1083		 * linked into a final BPF object file; instead of bailing
1084		 * out, report it into stderr, mark it as skipped, and
1085		 * proceed
1086		 */
1087		fprintf(stderr, "Failed to open '%s': %d\n", filename, -errno);
1088		env.files_skipped++;
1089		err = 0;
1090		goto cleanup;
1091	}
1092
1093	env.files_processed++;
1094
1095	bpf_object__for_each_program(prog, obj) {
1096		prog_cnt++;
1097	}
1098
1099	if (prog_cnt == 1) {
1100		prog = bpf_object__next_program(obj, NULL);
1101		bpf_program__set_autoload(prog, true);
1102		process_prog(filename, obj, prog);
1103		goto cleanup;
1104	}
1105
1106	bpf_object__for_each_program(prog, obj) {
1107		const char *prog_name = bpf_program__name(prog);
1108
1109		tobj = bpf_object__open_file(filename, &opts);
1110		if (!tobj) {
1111			err = -errno;
1112			fprintf(stderr, "Failed to open '%s': %d\n", filename, err);
1113			goto cleanup;
1114		}
1115
1116		lprog = NULL;
1117		bpf_object__for_each_program(tprog, tobj) {
1118			const char *tprog_name = bpf_program__name(tprog);
1119
1120			if (strcmp(prog_name, tprog_name) == 0) {
1121				bpf_program__set_autoload(tprog, true);
1122				lprog = tprog;
1123			} else {
1124				bpf_program__set_autoload(tprog, false);
1125			}
1126		}
1127
1128		process_prog(filename, tobj, lprog);
1129		bpf_object__close(tobj);
1130	}
1131
1132cleanup:
1133	bpf_object__close(obj);
1134	libbpf_set_print(old_libbpf_print_fn);
1135	return err;
1136}
1137
1138static int cmp_stat(const struct verif_stats *s1, const struct verif_stats *s2,
1139		    enum stat_id id, bool asc, bool abs)
1140{
1141	int cmp = 0;
1142
1143	switch (id) {
1144	case FILE_NAME:
1145		cmp = strcmp(s1->file_name, s2->file_name);
1146		break;
1147	case PROG_NAME:
1148		cmp = strcmp(s1->prog_name, s2->prog_name);
1149		break;
1150	case VERDICT:
1151	case DURATION:
1152	case TOTAL_INSNS:
1153	case TOTAL_STATES:
1154	case PEAK_STATES:
1155	case MAX_STATES_PER_INSN:
1156	case MARK_READ_MAX_LEN: {
1157		long v1 = s1->stats[id];
1158		long v2 = s2->stats[id];
1159
1160		if (abs) {
1161			v1 = v1 < 0 ? -v1 : v1;
1162			v2 = v2 < 0 ? -v2 : v2;
1163		}
1164
1165		if (v1 != v2)
1166			cmp = v1 < v2 ? -1 : 1;
1167		break;
1168	}
1169	default:
1170		fprintf(stderr, "Unrecognized stat #%d\n", id);
1171		exit(1);
1172	}
1173
1174	return asc ? cmp : -cmp;
1175}
1176
1177static int cmp_prog_stats(const void *v1, const void *v2)
1178{
1179	const struct verif_stats *s1 = v1, *s2 = v2;
1180	int i, cmp;
1181
1182	for (i = 0; i < env.sort_spec.spec_cnt; i++) {
1183		cmp = cmp_stat(s1, s2, env.sort_spec.ids[i],
1184			       env.sort_spec.asc[i], env.sort_spec.abs[i]);
1185		if (cmp != 0)
1186			return cmp;
1187	}
1188
1189	/* always disambiguate with file+prog, which are unique */
1190	cmp = strcmp(s1->file_name, s2->file_name);
1191	if (cmp != 0)
1192		return cmp;
1193	return strcmp(s1->prog_name, s2->prog_name);
1194}
1195
1196static void fetch_join_stat_value(const struct verif_stats_join *s,
1197				  enum stat_id id, enum stat_variant var,
1198				  const char **str_val,
1199				  double *num_val)
1200{
1201	long v1, v2;
1202
1203	if (id == FILE_NAME) {
1204		*str_val = s->file_name;
1205		return;
1206	}
1207	if (id == PROG_NAME) {
1208		*str_val = s->prog_name;
1209		return;
1210	}
1211
1212	v1 = s->stats_a ? s->stats_a->stats[id] : 0;
1213	v2 = s->stats_b ? s->stats_b->stats[id] : 0;
1214
1215	switch (var) {
1216	case VARIANT_A:
1217		if (!s->stats_a)
1218			*num_val = -DBL_MAX;
1219		else
1220			*num_val = s->stats_a->stats[id];
1221		return;
1222	case VARIANT_B:
1223		if (!s->stats_b)
1224			*num_val = -DBL_MAX;
1225		else
1226			*num_val = s->stats_b->stats[id];
1227		return;
1228	case VARIANT_DIFF:
1229		if (!s->stats_a || !s->stats_b)
1230			*num_val = -DBL_MAX;
1231		else if (id == VERDICT)
1232			*num_val = v1 == v2 ? 1.0 /* MATCH */ : 0.0 /* MISMATCH */;
1233		else
1234			*num_val = (double)(v2 - v1);
1235		return;
1236	case VARIANT_PCT:
1237		if (!s->stats_a || !s->stats_b) {
1238			*num_val = -DBL_MAX;
1239		} else if (v1 == 0) {
1240			if (v1 == v2)
1241				*num_val = 0.0;
1242			else
1243				*num_val = v2 < v1 ? -100.0 : 100.0;
1244		} else {
1245			 *num_val = (v2 - v1) * 100.0 / v1;
1246		}
1247		return;
1248	}
1249}
1250
1251static int cmp_join_stat(const struct verif_stats_join *s1,
1252			 const struct verif_stats_join *s2,
1253			 enum stat_id id, enum stat_variant var,
1254			 bool asc, bool abs)
1255{
1256	const char *str1 = NULL, *str2 = NULL;
1257	double v1 = 0.0, v2 = 0.0;
1258	int cmp = 0;
1259
1260	fetch_join_stat_value(s1, id, var, &str1, &v1);
1261	fetch_join_stat_value(s2, id, var, &str2, &v2);
1262
1263	if (abs) {
1264		v1 = fabs(v1);
1265		v2 = fabs(v2);
1266	}
1267
1268	if (str1)
1269		cmp = strcmp(str1, str2);
1270	else if (v1 != v2)
1271		cmp = v1 < v2 ? -1 : 1;
1272
1273	return asc ? cmp : -cmp;
1274}
1275
1276static int cmp_join_stats(const void *v1, const void *v2)
1277{
1278	const struct verif_stats_join *s1 = v1, *s2 = v2;
1279	int i, cmp;
1280
1281	for (i = 0; i < env.sort_spec.spec_cnt; i++) {
1282		cmp = cmp_join_stat(s1, s2,
1283				    env.sort_spec.ids[i],
1284				    env.sort_spec.variants[i],
1285				    env.sort_spec.asc[i],
1286				    env.sort_spec.abs[i]);
1287		if (cmp != 0)
1288			return cmp;
1289	}
1290
1291	/* always disambiguate with file+prog, which are unique */
1292	cmp = strcmp(s1->file_name, s2->file_name);
1293	if (cmp != 0)
1294		return cmp;
1295	return strcmp(s1->prog_name, s2->prog_name);
1296}
1297
1298#define HEADER_CHAR '-'
1299#define COLUMN_SEP "  "
1300
1301static void output_header_underlines(void)
1302{
1303	int i, j, len;
1304
1305	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1306		len = env.output_spec.lens[i];
1307
1308		printf("%s", i == 0 ? "" : COLUMN_SEP);
1309		for (j = 0; j < len; j++)
1310			printf("%c", HEADER_CHAR);
1311	}
1312	printf("\n");
1313}
1314
1315static void output_headers(enum resfmt fmt)
1316{
1317	const char *fmt_str;
1318	int i, len;
1319
1320	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1321		int id = env.output_spec.ids[i];
1322		int *max_len = &env.output_spec.lens[i];
1323
1324		switch (fmt) {
1325		case RESFMT_TABLE_CALCLEN:
1326			len = snprintf(NULL, 0, "%s", stat_defs[id].header);
1327			if (len > *max_len)
1328				*max_len = len;
1329			break;
1330		case RESFMT_TABLE:
1331			fmt_str = stat_defs[id].left_aligned ? "%s%-*s" : "%s%*s";
1332			printf(fmt_str, i == 0 ? "" : COLUMN_SEP,  *max_len, stat_defs[id].header);
1333			if (i == env.output_spec.spec_cnt - 1)
1334				printf("\n");
1335			break;
1336		case RESFMT_CSV:
1337			printf("%s%s", i == 0 ? "" : ",", stat_defs[id].names[0]);
1338			if (i == env.output_spec.spec_cnt - 1)
1339				printf("\n");
1340			break;
1341		}
1342	}
1343
1344	if (fmt == RESFMT_TABLE)
1345		output_header_underlines();
1346}
1347
1348static void prepare_value(const struct verif_stats *s, enum stat_id id,
1349			  const char **str, long *val)
1350{
1351	switch (id) {
1352	case FILE_NAME:
1353		*str = s ? s->file_name : "N/A";
1354		break;
1355	case PROG_NAME:
1356		*str = s ? s->prog_name : "N/A";
1357		break;
1358	case VERDICT:
1359		if (!s)
1360			*str = "N/A";
1361		else
1362			*str = s->stats[VERDICT] ? "success" : "failure";
1363		break;
1364	case DURATION:
1365	case TOTAL_INSNS:
1366	case TOTAL_STATES:
1367	case PEAK_STATES:
1368	case MAX_STATES_PER_INSN:
1369	case MARK_READ_MAX_LEN:
1370		*val = s ? s->stats[id] : 0;
1371		break;
1372	default:
1373		fprintf(stderr, "Unrecognized stat #%d\n", id);
1374		exit(1);
1375	}
1376}
1377
1378static void output_stats(const struct verif_stats *s, enum resfmt fmt, bool last)
1379{
1380	int i;
1381
1382	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1383		int id = env.output_spec.ids[i];
1384		int *max_len = &env.output_spec.lens[i], len;
1385		const char *str = NULL;
1386		long val = 0;
1387
1388		prepare_value(s, id, &str, &val);
1389
1390		switch (fmt) {
1391		case RESFMT_TABLE_CALCLEN:
1392			if (str)
1393				len = snprintf(NULL, 0, "%s", str);
1394			else
1395				len = snprintf(NULL, 0, "%ld", val);
1396			if (len > *max_len)
1397				*max_len = len;
1398			break;
1399		case RESFMT_TABLE:
1400			if (str)
1401				printf("%s%-*s", i == 0 ? "" : COLUMN_SEP, *max_len, str);
1402			else
1403				printf("%s%*ld", i == 0 ? "" : COLUMN_SEP,  *max_len, val);
1404			if (i == env.output_spec.spec_cnt - 1)
1405				printf("\n");
1406			break;
1407		case RESFMT_CSV:
1408			if (str)
1409				printf("%s%s", i == 0 ? "" : ",", str);
1410			else
1411				printf("%s%ld", i == 0 ? "" : ",", val);
1412			if (i == env.output_spec.spec_cnt - 1)
1413				printf("\n");
1414			break;
1415		}
1416	}
1417
1418	if (last && fmt == RESFMT_TABLE) {
1419		output_header_underlines();
1420		printf("Done. Processed %d files, %d programs. Skipped %d files, %d programs.\n",
1421		       env.files_processed, env.files_skipped, env.progs_processed, env.progs_skipped);
1422	}
1423}
1424
1425static int parse_stat_value(const char *str, enum stat_id id, struct verif_stats *st)
1426{
1427	switch (id) {
1428	case FILE_NAME:
1429		st->file_name = strdup(str);
1430		if (!st->file_name)
1431			return -ENOMEM;
1432		break;
1433	case PROG_NAME:
1434		st->prog_name = strdup(str);
1435		if (!st->prog_name)
1436			return -ENOMEM;
1437		break;
1438	case VERDICT:
1439		if (strcmp(str, "success") == 0) {
1440			st->stats[VERDICT] = true;
1441		} else if (strcmp(str, "failure") == 0) {
1442			st->stats[VERDICT] = false;
1443		} else {
1444			fprintf(stderr, "Unrecognized verification verdict '%s'\n", str);
1445			return -EINVAL;
1446		}
1447		break;
1448	case DURATION:
1449	case TOTAL_INSNS:
1450	case TOTAL_STATES:
1451	case PEAK_STATES:
1452	case MAX_STATES_PER_INSN:
1453	case MARK_READ_MAX_LEN: {
1454		long val;
1455		int err, n;
1456
1457		if (sscanf(str, "%ld %n", &val, &n) != 1 || n != strlen(str)) {
1458			err = -errno;
1459			fprintf(stderr, "Failed to parse '%s' as integer\n", str);
1460			return err;
1461		}
1462
1463		st->stats[id] = val;
1464		break;
1465	}
1466	default:
1467		fprintf(stderr, "Unrecognized stat #%d\n", id);
1468		return -EINVAL;
1469	}
1470	return 0;
1471}
1472
1473static int parse_stats_csv(const char *filename, struct stat_specs *specs,
1474			   struct verif_stats **statsp, int *stat_cntp)
1475{
1476	char line[4096];
1477	FILE *f;
1478	int err = 0;
1479	bool header = true;
1480
1481	f = fopen(filename, "r");
1482	if (!f) {
1483		err = -errno;
1484		fprintf(stderr, "Failed to open '%s': %d\n", filename, err);
1485		return err;
1486	}
1487
1488	*stat_cntp = 0;
1489
1490	while (fgets(line, sizeof(line), f)) {
1491		char *input = line, *state = NULL, *next;
1492		struct verif_stats *st = NULL;
1493		int col = 0;
1494
1495		if (!header) {
1496			void *tmp;
1497
1498			tmp = realloc(*statsp, (*stat_cntp + 1) * sizeof(**statsp));
1499			if (!tmp) {
1500				err = -ENOMEM;
1501				goto cleanup;
1502			}
1503			*statsp = tmp;
1504
1505			st = &(*statsp)[*stat_cntp];
1506			memset(st, 0, sizeof(*st));
1507
1508			*stat_cntp += 1;
1509		}
1510
1511		while ((next = strtok_r(state ? NULL : input, ",\n", &state))) {
1512			if (header) {
1513				/* for the first line, set up spec stats */
1514				err = parse_stat(next, specs);
1515				if (err)
1516					goto cleanup;
1517				continue;
1518			}
1519
1520			/* for all other lines, parse values based on spec */
1521			if (col >= specs->spec_cnt) {
1522				fprintf(stderr, "Found extraneous column #%d in row #%d of '%s'\n",
1523					col, *stat_cntp, filename);
1524				err = -EINVAL;
1525				goto cleanup;
1526			}
1527			err = parse_stat_value(next, specs->ids[col], st);
1528			if (err)
1529				goto cleanup;
1530			col++;
1531		}
1532
1533		if (header) {
1534			header = false;
1535			continue;
1536		}
1537
1538		if (col < specs->spec_cnt) {
1539			fprintf(stderr, "Not enough columns in row #%d in '%s'\n",
1540				*stat_cntp, filename);
1541			err = -EINVAL;
1542			goto cleanup;
1543		}
1544
1545		if (!st->file_name || !st->prog_name) {
1546			fprintf(stderr, "Row #%d in '%s' is missing file and/or program name\n",
1547				*stat_cntp, filename);
1548			err = -EINVAL;
1549			goto cleanup;
1550		}
1551
1552		/* in comparison mode we can only check filters after we
1553		 * parsed entire line; if row should be ignored we pretend we
1554		 * never parsed it
1555		 */
1556		if (!should_process_file_prog(st->file_name, st->prog_name)) {
1557			free(st->file_name);
1558			free(st->prog_name);
1559			*stat_cntp -= 1;
1560		}
1561	}
1562
1563	if (!feof(f)) {
1564		err = -errno;
1565		fprintf(stderr, "Failed I/O for '%s': %d\n", filename, err);
1566	}
1567
1568cleanup:
1569	fclose(f);
1570	return err;
1571}
1572
1573/* empty/zero stats for mismatched rows */
1574static const struct verif_stats fallback_stats = { .file_name = "", .prog_name = "" };
1575
1576static bool is_key_stat(enum stat_id id)
1577{
1578	return id == FILE_NAME || id == PROG_NAME;
1579}
1580
1581static void output_comp_header_underlines(void)
1582{
1583	int i, j, k;
1584
1585	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1586		int id = env.output_spec.ids[i];
1587		int max_j = is_key_stat(id) ? 1 : 3;
1588
1589		for (j = 0; j < max_j; j++) {
1590			int len = env.output_spec.lens[3 * i + j];
1591
1592			printf("%s", i + j == 0 ? "" : COLUMN_SEP);
1593
1594			for (k = 0; k < len; k++)
1595				printf("%c", HEADER_CHAR);
1596		}
1597	}
1598	printf("\n");
1599}
1600
1601static void output_comp_headers(enum resfmt fmt)
1602{
1603	static const char *table_sfxs[3] = {" (A)", " (B)", " (DIFF)"};
1604	static const char *name_sfxs[3] = {"_base", "_comp", "_diff"};
1605	int i, j, len;
1606
1607	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1608		int id = env.output_spec.ids[i];
1609		/* key stats don't have A/B/DIFF columns, they are common for both data sets */
1610		int max_j = is_key_stat(id) ? 1 : 3;
1611
1612		for (j = 0; j < max_j; j++) {
1613			int *max_len = &env.output_spec.lens[3 * i + j];
1614			bool last = (i == env.output_spec.spec_cnt - 1) && (j == max_j - 1);
1615			const char *sfx;
1616
1617			switch (fmt) {
1618			case RESFMT_TABLE_CALCLEN:
1619				sfx = is_key_stat(id) ? "" : table_sfxs[j];
1620				len = snprintf(NULL, 0, "%s%s", stat_defs[id].header, sfx);
1621				if (len > *max_len)
1622					*max_len = len;
1623				break;
1624			case RESFMT_TABLE:
1625				sfx = is_key_stat(id) ? "" : table_sfxs[j];
1626				printf("%s%-*s%s", i + j == 0 ? "" : COLUMN_SEP,
1627				       *max_len - (int)strlen(sfx), stat_defs[id].header, sfx);
1628				if (last)
1629					printf("\n");
1630				break;
1631			case RESFMT_CSV:
1632				sfx = is_key_stat(id) ? "" : name_sfxs[j];
1633				printf("%s%s%s", i + j == 0 ? "" : ",", stat_defs[id].names[0], sfx);
1634				if (last)
1635					printf("\n");
1636				break;
1637			}
1638		}
1639	}
1640
1641	if (fmt == RESFMT_TABLE)
1642		output_comp_header_underlines();
1643}
1644
1645static void output_comp_stats(const struct verif_stats_join *join_stats,
1646			      enum resfmt fmt, bool last)
1647{
1648	const struct verif_stats *base = join_stats->stats_a;
1649	const struct verif_stats *comp = join_stats->stats_b;
1650	char base_buf[1024] = {}, comp_buf[1024] = {}, diff_buf[1024] = {};
1651	int i;
1652
1653	for (i = 0; i < env.output_spec.spec_cnt; i++) {
1654		int id = env.output_spec.ids[i], len;
1655		int *max_len_base = &env.output_spec.lens[3 * i + 0];
1656		int *max_len_comp = &env.output_spec.lens[3 * i + 1];
1657		int *max_len_diff = &env.output_spec.lens[3 * i + 2];
1658		const char *base_str = NULL, *comp_str = NULL;
1659		long base_val = 0, comp_val = 0, diff_val = 0;
1660
1661		prepare_value(base, id, &base_str, &base_val);
1662		prepare_value(comp, id, &comp_str, &comp_val);
1663
1664		/* normalize all the outputs to be in string buffers for simplicity */
1665		if (is_key_stat(id)) {
1666			/* key stats (file and program name) are always strings */
1667			if (base)
1668				snprintf(base_buf, sizeof(base_buf), "%s", base_str);
1669			else
1670				snprintf(base_buf, sizeof(base_buf), "%s", comp_str);
1671		} else if (base_str) {
1672			snprintf(base_buf, sizeof(base_buf), "%s", base_str);
1673			snprintf(comp_buf, sizeof(comp_buf), "%s", comp_str);
1674			if (!base || !comp)
1675				snprintf(diff_buf, sizeof(diff_buf), "%s", "N/A");
1676			else if (strcmp(base_str, comp_str) == 0)
1677				snprintf(diff_buf, sizeof(diff_buf), "%s", "MATCH");
1678			else
1679				snprintf(diff_buf, sizeof(diff_buf), "%s", "MISMATCH");
1680		} else {
1681			double p = 0.0;
1682
1683			if (base)
1684				snprintf(base_buf, sizeof(base_buf), "%ld", base_val);
1685			else
1686				snprintf(base_buf, sizeof(base_buf), "%s", "N/A");
1687			if (comp)
1688				snprintf(comp_buf, sizeof(comp_buf), "%ld", comp_val);
1689			else
1690				snprintf(comp_buf, sizeof(comp_buf), "%s", "N/A");
1691
1692			diff_val = comp_val - base_val;
1693			if (!base || !comp) {
1694				snprintf(diff_buf, sizeof(diff_buf), "%s", "N/A");
1695			} else {
1696				if (base_val == 0) {
1697					if (comp_val == base_val)
1698						p = 0.0; /* avoid +0 (+100%) case */
1699					else
1700						p = comp_val < base_val ? -100.0 : 100.0;
1701				} else {
1702					 p = diff_val * 100.0 / base_val;
1703				}
1704				snprintf(diff_buf, sizeof(diff_buf), "%+ld (%+.2lf%%)", diff_val, p);
1705			}
1706		}
1707
1708		switch (fmt) {
1709		case RESFMT_TABLE_CALCLEN:
1710			len = strlen(base_buf);
1711			if (len > *max_len_base)
1712				*max_len_base = len;
1713			if (!is_key_stat(id)) {
1714				len = strlen(comp_buf);
1715				if (len > *max_len_comp)
1716					*max_len_comp = len;
1717				len = strlen(diff_buf);
1718				if (len > *max_len_diff)
1719					*max_len_diff = len;
1720			}
1721			break;
1722		case RESFMT_TABLE: {
1723			/* string outputs are left-aligned, number outputs are right-aligned */
1724			const char *fmt = base_str ? "%s%-*s" : "%s%*s";
1725
1726			printf(fmt, i == 0 ? "" : COLUMN_SEP, *max_len_base, base_buf);
1727			if (!is_key_stat(id)) {
1728				printf(fmt, COLUMN_SEP, *max_len_comp, comp_buf);
1729				printf(fmt, COLUMN_SEP, *max_len_diff, diff_buf);
1730			}
1731			if (i == env.output_spec.spec_cnt - 1)
1732				printf("\n");
1733			break;
1734		}
1735		case RESFMT_CSV:
1736			printf("%s%s", i == 0 ? "" : ",", base_buf);
1737			if (!is_key_stat(id)) {
1738				printf("%s%s", i == 0 ? "" : ",", comp_buf);
1739				printf("%s%s", i == 0 ? "" : ",", diff_buf);
1740			}
1741			if (i == env.output_spec.spec_cnt - 1)
1742				printf("\n");
1743			break;
1744		}
1745	}
1746
1747	if (last && fmt == RESFMT_TABLE)
1748		output_comp_header_underlines();
1749}
1750
1751static int cmp_stats_key(const struct verif_stats *base, const struct verif_stats *comp)
1752{
1753	int r;
1754
1755	r = strcmp(base->file_name, comp->file_name);
1756	if (r != 0)
1757		return r;
1758	return strcmp(base->prog_name, comp->prog_name);
1759}
1760
1761static bool is_join_stat_filter_matched(struct filter *f, const struct verif_stats_join *stats)
1762{
1763	static const double eps = 1e-9;
1764	const char *str = NULL;
1765	double value = 0.0;
1766
1767	fetch_join_stat_value(stats, f->stat_id, f->stat_var, &str, &value);
1768
1769	if (f->abs)
1770		value = fabs(value);
1771
1772	switch (f->op) {
1773	case OP_EQ: return value > f->value - eps && value < f->value + eps;
1774	case OP_NEQ: return value < f->value - eps || value > f->value + eps;
1775	case OP_LT: return value < f->value - eps;
1776	case OP_LE: return value <= f->value + eps;
1777	case OP_GT: return value > f->value + eps;
1778	case OP_GE: return value >= f->value - eps;
1779	}
1780
1781	fprintf(stderr, "BUG: unknown filter op %d!\n", f->op);
1782	return false;
1783}
1784
1785static bool should_output_join_stats(const struct verif_stats_join *stats)
1786{
1787	struct filter *f;
1788	int i, allow_cnt = 0;
1789
1790	for (i = 0; i < env.deny_filter_cnt; i++) {
1791		f = &env.deny_filters[i];
1792		if (f->kind != FILTER_STAT)
1793			continue;
1794
1795		if (is_join_stat_filter_matched(f, stats))
1796			return false;
1797	}
1798
1799	for (i = 0; i < env.allow_filter_cnt; i++) {
1800		f = &env.allow_filters[i];
1801		if (f->kind != FILTER_STAT)
1802			continue;
1803		allow_cnt++;
1804
1805		if (is_join_stat_filter_matched(f, stats))
1806			return true;
1807	}
1808
1809	/* if there are no stat allowed filters, pass everything through */
1810	return allow_cnt == 0;
1811}
1812
1813static int handle_comparison_mode(void)
1814{
1815	struct stat_specs base_specs = {}, comp_specs = {};
1816	struct stat_specs tmp_sort_spec;
1817	enum resfmt cur_fmt;
1818	int err, i, j, last_idx, cnt;
1819
1820	if (env.filename_cnt != 2) {
1821		fprintf(stderr, "Comparison mode expects exactly two input CSV files!\n\n");
1822		argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat");
1823		return -EINVAL;
1824	}
1825
1826	err = parse_stats_csv(env.filenames[0], &base_specs,
1827			      &env.baseline_stats, &env.baseline_stat_cnt);
1828	if (err) {
1829		fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[0], err);
1830		return err;
1831	}
1832	err = parse_stats_csv(env.filenames[1], &comp_specs,
1833			      &env.prog_stats, &env.prog_stat_cnt);
1834	if (err) {
1835		fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[1], err);
1836		return err;
1837	}
1838
1839	/* To keep it simple we validate that the set and order of stats in
1840	 * both CSVs are exactly the same. This can be lifted with a bit more
1841	 * pre-processing later.
1842	 */
1843	if (base_specs.spec_cnt != comp_specs.spec_cnt) {
1844		fprintf(stderr, "Number of stats in '%s' and '%s' differs (%d != %d)!\n",
1845			env.filenames[0], env.filenames[1],
1846			base_specs.spec_cnt, comp_specs.spec_cnt);
1847		return -EINVAL;
1848	}
1849	for (i = 0; i < base_specs.spec_cnt; i++) {
1850		if (base_specs.ids[i] != comp_specs.ids[i]) {
1851			fprintf(stderr, "Stats composition differs between '%s' and '%s' (%s != %s)!\n",
1852				env.filenames[0], env.filenames[1],
1853				stat_defs[base_specs.ids[i]].names[0],
1854				stat_defs[comp_specs.ids[i]].names[0]);
1855			return -EINVAL;
1856		}
1857	}
1858
1859	/* Replace user-specified sorting spec with file+prog sorting rule to
1860	 * be able to join two datasets correctly. Once we are done, we will
1861	 * restore the original sort spec.
1862	 */
1863	tmp_sort_spec = env.sort_spec;
1864	env.sort_spec = join_sort_spec;
1865	qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats);
1866	qsort(env.baseline_stats, env.baseline_stat_cnt, sizeof(*env.baseline_stats), cmp_prog_stats);
1867	env.sort_spec = tmp_sort_spec;
1868
1869	/* Join two datasets together. If baseline and comparison datasets
1870	 * have different subset of rows (we match by 'object + prog' as
1871	 * a unique key) then assume empty/missing/zero value for rows that
1872	 * are missing in the opposite data set.
1873	 */
1874	i = j = 0;
1875	while (i < env.baseline_stat_cnt || j < env.prog_stat_cnt) {
1876		const struct verif_stats *base, *comp;
1877		struct verif_stats_join *join;
1878		void *tmp;
1879		int r;
1880
1881		base = i < env.baseline_stat_cnt ? &env.baseline_stats[i] : &fallback_stats;
1882		comp = j < env.prog_stat_cnt ? &env.prog_stats[j] : &fallback_stats;
1883
1884		if (!base->file_name || !base->prog_name) {
1885			fprintf(stderr, "Entry #%d in '%s' doesn't have file and/or program name specified!\n",
1886				i, env.filenames[0]);
1887			return -EINVAL;
1888		}
1889		if (!comp->file_name || !comp->prog_name) {
1890			fprintf(stderr, "Entry #%d in '%s' doesn't have file and/or program name specified!\n",
1891				j, env.filenames[1]);
1892			return -EINVAL;
1893		}
1894
1895		tmp = realloc(env.join_stats, (env.join_stat_cnt + 1) * sizeof(*env.join_stats));
1896		if (!tmp)
1897			return -ENOMEM;
1898		env.join_stats = tmp;
1899
1900		join = &env.join_stats[env.join_stat_cnt];
1901		memset(join, 0, sizeof(*join));
1902
1903		r = cmp_stats_key(base, comp);
1904		if (r == 0) {
1905			join->file_name = base->file_name;
1906			join->prog_name = base->prog_name;
1907			join->stats_a = base;
1908			join->stats_b = comp;
1909			i++;
1910			j++;
1911		} else if (base != &fallback_stats && (comp == &fallback_stats || r < 0)) {
1912			join->file_name = base->file_name;
1913			join->prog_name = base->prog_name;
1914			join->stats_a = base;
1915			join->stats_b = NULL;
1916			i++;
1917		} else if (comp != &fallback_stats && (base == &fallback_stats || r > 0)) {
1918			join->file_name = comp->file_name;
1919			join->prog_name = comp->prog_name;
1920			join->stats_a = NULL;
1921			join->stats_b = comp;
1922			j++;
1923		} else {
1924			fprintf(stderr, "%s:%d: should never reach here i=%i, j=%i",
1925				__FILE__, __LINE__, i, j);
1926			return -EINVAL;
1927		}
1928		env.join_stat_cnt += 1;
1929	}
1930
1931	/* now sort joined results according to sort spec */
1932	qsort(env.join_stats, env.join_stat_cnt, sizeof(*env.join_stats), cmp_join_stats);
1933
1934	/* for human-readable table output we need to do extra pass to
1935	 * calculate column widths, so we substitute current output format
1936	 * with RESFMT_TABLE_CALCLEN and later revert it back to RESFMT_TABLE
1937	 * and do everything again.
1938	 */
1939	if (env.out_fmt == RESFMT_TABLE)
1940		cur_fmt = RESFMT_TABLE_CALCLEN;
1941	else
1942		cur_fmt = env.out_fmt;
1943
1944one_more_time:
1945	output_comp_headers(cur_fmt);
1946
1947	last_idx = -1;
1948	cnt = 0;
1949	for (i = 0; i < env.join_stat_cnt; i++) {
1950		const struct verif_stats_join *join = &env.join_stats[i];
1951
1952		if (!should_output_join_stats(join))
1953			continue;
1954
1955		if (env.top_n && cnt >= env.top_n)
1956			break;
1957
1958		if (cur_fmt == RESFMT_TABLE_CALCLEN)
1959			last_idx = i;
1960
1961		output_comp_stats(join, cur_fmt, i == last_idx);
1962
1963		cnt++;
1964	}
1965
1966	if (cur_fmt == RESFMT_TABLE_CALCLEN) {
1967		cur_fmt = RESFMT_TABLE;
1968		goto one_more_time; /* ... this time with feeling */
1969	}
1970
1971	return 0;
1972}
1973
1974static bool is_stat_filter_matched(struct filter *f, const struct verif_stats *stats)
1975{
1976	long value = stats->stats[f->stat_id];
1977
1978	if (f->abs)
1979		value = value < 0 ? -value : value;
1980
1981	switch (f->op) {
1982	case OP_EQ: return value == f->value;
1983	case OP_NEQ: return value != f->value;
1984	case OP_LT: return value < f->value;
1985	case OP_LE: return value <= f->value;
1986	case OP_GT: return value > f->value;
1987	case OP_GE: return value >= f->value;
1988	}
1989
1990	fprintf(stderr, "BUG: unknown filter op %d!\n", f->op);
1991	return false;
1992}
1993
1994static bool should_output_stats(const struct verif_stats *stats)
1995{
1996	struct filter *f;
1997	int i, allow_cnt = 0;
1998
1999	for (i = 0; i < env.deny_filter_cnt; i++) {
2000		f = &env.deny_filters[i];
2001		if (f->kind != FILTER_STAT)
2002			continue;
2003
2004		if (is_stat_filter_matched(f, stats))
2005			return false;
2006	}
2007
2008	for (i = 0; i < env.allow_filter_cnt; i++) {
2009		f = &env.allow_filters[i];
2010		if (f->kind != FILTER_STAT)
2011			continue;
2012		allow_cnt++;
2013
2014		if (is_stat_filter_matched(f, stats))
2015			return true;
2016	}
2017
2018	/* if there are no stat allowed filters, pass everything through */
2019	return allow_cnt == 0;
2020}
2021
2022static void output_prog_stats(void)
2023{
2024	const struct verif_stats *stats;
2025	int i, last_stat_idx = 0, cnt = 0;
2026
2027	if (env.out_fmt == RESFMT_TABLE) {
2028		/* calculate column widths */
2029		output_headers(RESFMT_TABLE_CALCLEN);
2030		for (i = 0; i < env.prog_stat_cnt; i++) {
2031			stats = &env.prog_stats[i];
2032			if (!should_output_stats(stats))
2033				continue;
2034			output_stats(stats, RESFMT_TABLE_CALCLEN, false);
2035			last_stat_idx = i;
2036		}
2037	}
2038
2039	/* actually output the table */
2040	output_headers(env.out_fmt);
2041	for (i = 0; i < env.prog_stat_cnt; i++) {
2042		stats = &env.prog_stats[i];
2043		if (!should_output_stats(stats))
2044			continue;
2045		if (env.top_n && cnt >= env.top_n)
2046			break;
2047		output_stats(stats, env.out_fmt, i == last_stat_idx);
2048		cnt++;
2049	}
2050}
2051
2052static int handle_verif_mode(void)
2053{
2054	int i, err;
2055
2056	if (env.filename_cnt == 0) {
2057		fprintf(stderr, "Please provide path to BPF object file!\n\n");
2058		argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat");
2059		return -EINVAL;
2060	}
2061
2062	for (i = 0; i < env.filename_cnt; i++) {
2063		err = process_obj(env.filenames[i]);
2064		if (err) {
2065			fprintf(stderr, "Failed to process '%s': %d\n", env.filenames[i], err);
2066			return err;
2067		}
2068	}
2069
2070	qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats);
2071
2072	output_prog_stats();
2073
2074	return 0;
2075}
2076
2077static int handle_replay_mode(void)
2078{
2079	struct stat_specs specs = {};
2080	int err;
2081
2082	if (env.filename_cnt != 1) {
2083		fprintf(stderr, "Replay mode expects exactly one input CSV file!\n\n");
2084		argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat");
2085		return -EINVAL;
2086	}
2087
2088	err = parse_stats_csv(env.filenames[0], &specs,
2089			      &env.prog_stats, &env.prog_stat_cnt);
2090	if (err) {
2091		fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[0], err);
2092		return err;
2093	}
2094
2095	qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats);
2096
2097	output_prog_stats();
2098
2099	return 0;
2100}
2101
2102int main(int argc, char **argv)
2103{
2104	int err = 0, i;
2105
2106	if (argp_parse(&argp, argc, argv, 0, NULL, NULL))
2107		return 1;
2108
2109	if (env.show_version) {
2110		printf("%s\n", argp_program_version);
2111		return 0;
2112	}
2113
2114	if (env.verbose && env.quiet) {
2115		fprintf(stderr, "Verbose and quiet modes are incompatible, please specify just one or neither!\n\n");
2116		argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat");
2117		return 1;
2118	}
2119	if (env.verbose && env.log_level == 0)
2120		env.log_level = 1;
2121
2122	if (env.output_spec.spec_cnt == 0) {
2123		if (env.out_fmt == RESFMT_CSV)
2124			env.output_spec = default_csv_output_spec;
2125		else
2126			env.output_spec = default_output_spec;
2127	}
2128	if (env.sort_spec.spec_cnt == 0)
2129		env.sort_spec = default_sort_spec;
2130
2131	if (env.comparison_mode && env.replay_mode) {
2132		fprintf(stderr, "Can't specify replay and comparison mode at the same time!\n\n");
2133		argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat");
2134		return 1;
2135	}
2136
2137	if (env.comparison_mode)
2138		err = handle_comparison_mode();
2139	else if (env.replay_mode)
2140		err = handle_replay_mode();
2141	else
2142		err = handle_verif_mode();
2143
2144	free_verif_stats(env.prog_stats, env.prog_stat_cnt);
2145	free_verif_stats(env.baseline_stats, env.baseline_stat_cnt);
2146	free(env.join_stats);
2147	for (i = 0; i < env.filename_cnt; i++)
2148		free(env.filenames[i]);
2149	free(env.filenames);
2150	for (i = 0; i < env.allow_filter_cnt; i++) {
2151		free(env.allow_filters[i].any_glob);
2152		free(env.allow_filters[i].file_glob);
2153		free(env.allow_filters[i].prog_glob);
2154	}
2155	free(env.allow_filters);
2156	for (i = 0; i < env.deny_filter_cnt; i++) {
2157		free(env.deny_filters[i].any_glob);
2158		free(env.deny_filters[i].file_glob);
2159		free(env.deny_filters[i].prog_glob);
2160	}
2161	free(env.deny_filters);
2162	return -err;
2163}
2164