secnote

The secnote tool.
Commits | Files | Refs | README | LICENSE | git clone https://git.kore.io/secnote

secnote.c (22853B)



      1 /*
      2  * Copyright (c) 2022 Joris Vink <joris@coders.se>
      3  *
      4  * Permission to use, copy, modify, and distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 #include <sys/types.h>
     18 #include <sys/stat.h>
     19 #include <sys/wait.h>
     20 #include <sys/queue.h>
     21 
     22 #include <openssl/sha.h>
     23 
     24 #include <ctype.h>
     25 #include <errno.h>
     26 #include <fnmatch.h>
     27 #include <fts.h>
     28 #include <limits.h>
     29 #include <stdarg.h>
     30 #include <stdio.h>
     31 #include <stdlib.h>
     32 #include <string.h>
     33 #include <unistd.h>
     34 
     35 #if defined(__linux__)
     36 #include <bsd/bsd.h>
     37 #endif
     38 
     39 #define VERSION			"0.1"
     40 
     41 #define FILE_TYPE_C		1
     42 #define FILE_TYPE_PYTHON	2
     43 
     44 #define DUMP_PARSE_TOPIC	1
     45 #define DUMP_PARSE_ENTRY	2
     46 
     47 #define ENTRY_STATE_MOVED	(1 << 1)
     48 #define ENTRY_STATE_DIFFERS	(1 << 2)
     49 #define ENTRY_STATE_GONE	(1 << 3)
     50 #define ENTRY_STATE_SAME	(1 << 4)
     51 #define ENTRY_STATE_RENAMED	(1 << 5)
     52 
     53 #define FILE_SEPARATOR		\
     54     "==================================================================="
     55 
     56 #define TAG_OPEN		"\x40secnote-open"
     57 #define TAG_CLOSE		"\x40secnote-close"
     58 
     59 #define MAX(a, b)		((a > b) ? a : b)
     60 #define MIN(a, b)		((a < b) ? a : b)
     61 
     62 struct line {
     63 	char			*code;
     64 	TAILQ_ENTRY(line)	list;
     65 };
     66 
     67 struct entry {
     68 	int			order;
     69 
     70 	char			*id;
     71 	char			*file;
     72 	char			*code;
     73 	char			*context;
     74 
     75 	int			line_start;
     76 	int			line_end;
     77 
     78 	SHA256_CTX		shactx;
     79 	char			digest[(SHA256_DIGEST_LENGTH * 2) + 1];
     80 
     81 	TAILQ_HEAD(, line)	lines;
     82 	TAILQ_ENTRY(entry)	list;
     83 };
     84 
     85 TAILQ_HEAD(entry_list, entry);
     86 
     87 struct topic {
     88 	char			*name;
     89 	struct entry_list	entries;
     90 	TAILQ_ENTRY(topic)	list;
     91 };
     92 
     93 struct file {
     94 	int			type;
     95 	int			line;
     96 
     97 	FILE			*fp;
     98 	char			**lc;
     99 	char			*path;
    100 	char			buf[512];
    101 };
    102 
    103 struct context {
    104 	int			db;
    105 	int			list;
    106 	int			pnum;
    107 	int			full;
    108 	const char		*query;
    109 
    110 	struct topic		*topic;
    111 	struct entry		*entry;
    112 
    113 	TAILQ_HEAD(, topic)	topics;
    114 };
    115 
    116 static char	*xstrdup(const char *);
    117 static void	fatal(const char *, ...);
    118 static int	filecmp(const FTSENT **, const FTSENT **);
    119 
    120 static void	entry_record_line(struct entry *, const char *);
    121 static int	entry_check_state(struct entry_list *, struct entry *,
    122 		    struct entry **);
    123 
    124 static int	note_parse_arguments(char *, int *, char **, char **);
    125 
    126 static void	file_close(struct file *);
    127 static int	file_read_line(struct file *);
    128 static void	file_consume_newline(struct file *);
    129 static void	file_cache_line(struct file *, char *);
    130 static void	file_parse(struct context *, const char *);
    131 static void	file_open(struct file *, const char *, const char *);
    132 
    133 static void	text_topic_dump(struct context *);
    134 static int	text_chunk_new_entries(struct topic *, int *);
    135 static void	text_topic_write(struct context *, struct topic *);
    136 static void	text_topic_header(struct context *, struct topic *);
    137 
    138 static void	load_from_dump(struct context *, const char *);
    139 static void	load_from_args(struct context *, int, char **);
    140 
    141 static int	dump_parse_topic(struct context *, struct file *);
    142 static int	dump_parse_entry(struct context *, struct file *);
    143 
    144 static void	topic_entry_free(struct entry *);
    145 static void	topic_free(struct context *, struct topic *);
    146 
    147 static void	context_compare(struct context *, struct context *);
    148 
    149 static struct topic	*topic_resolve(struct context *, const char *);
    150 static struct entry	*topic_record_entry(struct context *, struct topic *,
    151 			    const char *, const char *, const char *, int);
    152 
    153 static struct {
    154 	const char	*ext;
    155 	int		type;
    156 } extlist[] = {
    157 	{ ".c",		FILE_TYPE_C },
    158 	{ ".h",		FILE_TYPE_C },
    159 	{ ".py",	FILE_TYPE_PYTHON },
    160 	{ NULL,		-1 },
    161 };
    162 
    163 static void
    164 usage(void)
    165 {
    166 	fprintf(stderr,
    167 	    "Usage: secnote [-pnum] [-l [-f] | -d | -q query | -v db] [src]\n");
    168 	exit(1);
    169 }
    170 
    171 int
    172 main(int argc, char *argv[])
    173 {
    174 	int			ch;
    175 	struct context		ctx, vfy;
    176 	const char		*err, *verify;
    177 
    178 	verify = NULL;
    179 
    180 	memset(&ctx, 0, sizeof(ctx));
    181 	memset(&vfy, 0, sizeof(vfy));
    182 
    183 	TAILQ_INIT(&ctx.topics);
    184 	TAILQ_INIT(&vfy.topics);
    185 
    186 	while ((ch = getopt(argc, argv, "dfhlp:q:v:V")) != -1) {
    187 		switch (ch) {
    188 		case 'd':
    189 			ctx.db = 1;
    190 			ctx.list = 1;
    191 			ctx.full = 1;
    192 			break;
    193 		case 'f':
    194 			ctx.full = 1;
    195 			break;
    196 		case 'l':
    197 			ctx.list = 1;
    198 			break;
    199 		case 'p':
    200 			ctx.pnum = strtonum(optarg, 0, 255, &err);
    201 			if (err != NULL)
    202 				fatal("-p %s invalid: %s", optarg, err);
    203 			vfy.pnum = ctx.pnum;
    204 			break;
    205 		case 'q':
    206 			ctx.query = optarg;
    207 			break;
    208 		case 'v':
    209 			verify = optarg;
    210 			break;
    211 		case 'V':
    212 			fatal("secnote %s", VERSION);
    213 			/* NOTREACHED */
    214 		case 'h':
    215 		default:
    216 			usage();
    217 			/* NOTREACHED */
    218 		}
    219 	}
    220 
    221 	argc -= optind;
    222 	argv += optind;
    223 
    224 	if (argc < 1)
    225 		usage();
    226 
    227 	if (ctx.list && (ctx.query || verify))
    228 		fatal("-l/-d and -q/-v are mutually exclusive");
    229 
    230 	if (ctx.full && !ctx.list) {
    231 		fprintf(stderr, "-f only works with -l\n");
    232 		usage();
    233 	}
    234 
    235 	load_from_args(&ctx, argc, argv);
    236 
    237 	if (verify) {
    238 		load_from_dump(&vfy, verify);
    239 		context_compare(&vfy, &ctx);
    240 	} else {
    241 		if (TAILQ_EMPTY(&ctx.topics))
    242 			printf("no topics found\n");
    243 		else
    244 			text_topic_dump(&ctx);
    245 	}
    246 
    247 	return (0);
    248 }
    249 
    250 static void
    251 context_compare(struct context *verify, struct context *ondisk)
    252 {
    253 	struct topic		*t1, *t2;
    254 	struct entry		*entry, *ent;
    255 	int			a, b, changes, header, state;
    256 
    257 	changes = 0;
    258 
    259 	while ((t1 = TAILQ_FIRST(&verify->topics)) != NULL) {
    260 		TAILQ_FOREACH(t2, &ondisk->topics, list) {
    261 			if (!strcmp(t2->name, t1->name))
    262 				break;
    263 		}
    264 
    265 		if (t2 == NULL) {
    266 			changes++;
    267 			printf("topic '%s' not found in given source\n",
    268 			    t1->name);
    269 			topic_free(verify, t1);
    270 			if (TAILQ_EMPTY(&verify->topics))
    271 				printf("\n");
    272 			continue;
    273 		}
    274 
    275 		header = 0;
    276 
    277 		TAILQ_FOREACH(entry, &t1->entries, list) {
    278 			state = entry_check_state(&t2->entries, entry, &ent);
    279 
    280 			if (ent != NULL)
    281 				TAILQ_REMOVE(&t2->entries, ent, list);
    282 
    283 			if (state == ENTRY_STATE_SAME) {
    284 				if (ent != NULL)
    285 					topic_entry_free(ent);
    286 				continue;
    287 			}
    288 
    289 			if (!header) {
    290 				header = 1;
    291 				printf("%s\n", FILE_SEPARATOR);
    292 				printf("%s\n", t1->name);
    293 				printf("%s\n\n", FILE_SEPARATOR);
    294 			}
    295 
    296 			printf("    %s in %s:%d-%d\n", entry->id, entry->file,
    297 			    entry->line_start, entry->line_end);
    298 
    299 			switch (state) {
    300 			case ENTRY_STATE_RENAMED:
    301 				printf("      - renamed %s -> %s\n",
    302 				    entry->id, ent->id);
    303 				if (ent != NULL)
    304 					topic_entry_free(ent);
    305 				continue;
    306 			case ENTRY_STATE_GONE:
    307 				changes++;
    308 				printf("      - not found\n");
    309 				if (ent != NULL)
    310 					topic_entry_free(ent);
    311 				continue;
    312 			}
    313 
    314 			changes++;
    315 
    316 			a = entry->line_end - entry->line_start;
    317 			b = ent->line_end - ent->line_start;
    318 
    319 			if (state & ENTRY_STATE_MOVED) {
    320 				printf("      - moved to %d-%d\n",
    321 				    ent->line_start, ent->line_end);
    322 			}
    323 
    324 			if (entry->context != NULL && ent->context != NULL) {
    325 				if (strcmp(entry->context, ent->context)) {
    326 					printf("      - parent %s -> %s\n",
    327 					    entry->context, ent->context);
    328 				}
    329 			}
    330 
    331 			if (state & ENTRY_STATE_DIFFERS) {
    332 				printf("      - modified");
    333 
    334 				if (a != b)
    335 					printf(" %+d lines(s)", b - a);
    336 
    337 				printf("\n");
    338 			}
    339 
    340 			topic_entry_free(ent);
    341 
    342 			if (entry != TAILQ_LAST(&t1->entries, entry_list) ||
    343 			    !TAILQ_EMPTY(&t2->entries))
    344 				printf("\n");
    345 		}
    346 
    347 		changes += text_chunk_new_entries(t2, &header);
    348 
    349 		topic_free(verify, t1);
    350 		topic_free(ondisk, t2);
    351 
    352 		if (header) {
    353 			header = 0;
    354 			printf("\n");
    355 		}
    356 	}
    357 
    358 	while ((t1 = TAILQ_FIRST(&ondisk->topics)) != NULL) {
    359 		header = 0;
    360 		changes += text_chunk_new_entries(t1, &header);
    361 		topic_free(ondisk, t1);
    362 	}
    363 
    364 	if (changes > 0) {
    365 		fatal("%s%d change%s detected",
    366 		    header ? "\n" : "", changes, changes > 1 ? "s" : "");
    367 	}
    368 
    369 	printf("secnote identical\n");
    370 }
    371 
    372 static void
    373 load_from_args(struct context *ctx, int argc, char **argv)
    374 {
    375 	struct stat		st;
    376 	FTS			*fts;
    377 	FTSENT			*ent;
    378 	int			idx, j;
    379 	char			*pv[2], *ext;
    380 
    381 	for (idx = 0; idx < argc; idx++) {
    382 		if (stat(argv[idx], &st) == -1 ||
    383 		    access(argv[idx], R_OK) == -1) {
    384 			fprintf(stderr, "skipping '%s' (%s)\n",
    385 			    argv[idx], strerror(errno));
    386 			continue;
    387 		}
    388 
    389 		if (S_ISREG(st.st_mode)) {
    390 			file_parse(ctx, argv[idx]);
    391 			continue;
    392 		}
    393 
    394 		if (!S_ISDIR(st.st_mode)) {
    395 			fprintf(stderr, "skipping '%s'\n", argv[idx]);
    396 			continue;
    397 		}
    398 
    399 		pv[0] = argv[idx];
    400 		pv[1] = NULL;
    401 
    402 		fts = fts_open(pv,
    403 		    FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, filecmp);
    404 		if (fts == NULL)
    405 			fatal("fts_open: %s", strerror(errno));
    406 
    407 		while ((ent = fts_read(fts)) != NULL) {
    408 			if (!S_ISREG(ent->fts_statp->st_mode))
    409 				continue;
    410 
    411 			if ((ext = strrchr(ent->fts_name, '.')) == NULL)
    412 				continue;
    413 
    414 			for (j = 0; extlist[j].ext != NULL; j++) {
    415 				if (!strcmp(extlist[j].ext, ext))
    416 					break;
    417 			}
    418 
    419 			if (extlist[j].ext == NULL)
    420 				continue;
    421 
    422 			file_parse(ctx, ent->fts_path);
    423 		}
    424 
    425 		fts_close(fts);
    426 	}
    427 }
    428 
    429 static void
    430 load_from_dump(struct context *ctx, const char *path)
    431 {
    432 	struct file	file;
    433 	int		state;
    434 
    435 	if (!strcmp(path, "-")) {
    436 		file.fp = stdin;
    437 		file.path = "<stdin>";
    438 	} else {
    439 		file_open(&file, path, "r");
    440 	}
    441 
    442 	state = DUMP_PARSE_TOPIC;
    443 
    444 	while (file_read_line(&file)) {
    445 		switch (state) {
    446 		case DUMP_PARSE_TOPIC:
    447 			state = dump_parse_topic(ctx, &file);
    448 			break;
    449 		case DUMP_PARSE_ENTRY:
    450 			state = dump_parse_entry(ctx, &file);
    451 			break;
    452 		default:
    453 			fatal("invalid parse state %d", state);
    454 		}
    455 	}
    456 
    457 	if (file.fp != stdin)
    458 		file_close(&file);
    459 }
    460 
    461 static int
    462 dump_parse_topic(struct context *ctx, struct file *file)
    463 {
    464 	if (file->buf[0] != '@' || file->buf[1] != ' ') {
    465 		if (!strcmp(file->buf, "no topics found"))
    466 			return (DUMP_PARSE_TOPIC);
    467 		fatal("expected start of topic, got '%s'", file->buf);
    468 	}
    469 
    470 	ctx->topic = topic_resolve(ctx, &file->buf[2]);
    471 	file_consume_newline(file);
    472 
    473 	return (DUMP_PARSE_ENTRY);
    474 }
    475 
    476 static int
    477 dump_parse_entry(struct context *ctx, struct file *file)
    478 {
    479 	int		count;
    480 	struct entry	*entry;
    481 	char		**ap, *args[12];
    482 	char		*id, *line, *hash, *path, *region, *func;
    483 
    484 	if (file->buf[0] == '\0') {
    485 		ctx->topic = NULL;
    486 		ctx->entry = NULL;
    487 		return (DUMP_PARSE_TOPIC);
    488 	}
    489 
    490 	count = 0;
    491 	line = file->buf;
    492 
    493 	for (ap = args; ap < &args[12] &&
    494 	    (*ap = strsep(&line, ":")) != NULL;) {
    495 		if (**ap != '\0') {
    496 			ap++;
    497 			count++;
    498 		}
    499 	}
    500 
    501 	if (count != 4 && count != 5)
    502 		fatal("invalid entry in file '%s' (%d)", file->path, count);
    503 
    504 	hash = args[0];
    505 	id = args[1];
    506 	path = args[2];
    507 	region = args[3];
    508 
    509 	if (count > 4)
    510 		func = args[4];
    511 	else
    512 		func = NULL;
    513 
    514 	entry = topic_record_entry(ctx, ctx->topic, id, path, func, -1);
    515 
    516 	if (strlcpy(entry->digest, hash, sizeof(entry->digest)) >=
    517 	    sizeof(entry->digest))
    518 		fatal("invalid hash string '%s' in '%s'", hash, file->path);
    519 
    520 	if (sscanf(region, "%d-%d", &entry->line_start, &entry->line_end) != 2)
    521 		fatal("invalid region string '%s' in '%s'", region, file->path);
    522 
    523 	ctx->entry = entry;
    524 
    525 	return (DUMP_PARSE_ENTRY);
    526 }
    527 
    528 static void
    529 file_open(struct file *file, const char *path, const char *mode)
    530 {
    531 	int		i;
    532 	const char	*ext;
    533 
    534 	memset(file, 0, sizeof(*file));
    535 
    536 	if ((file->fp = fopen(path, mode)) == NULL)
    537 		fatal("fopen(%s): %s", path, strerror(errno));
    538 
    539 	file->path = xstrdup(path);
    540 
    541 	if ((ext = strrchr(path, '.')) == NULL)
    542 		return;
    543 
    544 	for (i = 0; extlist[i].ext != NULL; i++) {
    545 		if (!strcmp(extlist[i].ext, ext)) {
    546 			file->type = extlist[i].type;
    547 			break;
    548 		}
    549 	}
    550 }
    551 
    552 static void
    553 file_close(struct file *file)
    554 {
    555 	int		line;
    556 
    557 	if (file->line > 0 && file->lc != NULL) {
    558 		line = file->line - 1;
    559 
    560 		while (line >= 0) {
    561 			free(file->lc[line]);
    562 			line--;
    563 		}
    564 	}
    565 
    566 	fclose(file->fp);
    567 
    568 	free(file->lc);
    569 	free(file->path);
    570 }
    571 
    572 static void
    573 file_cache_line(struct file *file, char *buf)
    574 {
    575 	size_t		newsz;
    576 
    577 	newsz = sizeof(char *) * (file->line + 1);
    578 	if ((file->lc = realloc(file->lc, newsz)) == NULL)
    579 		fatal("realloc(%zu): %s", newsz, strerror(errno));
    580 
    581 	file->lc[file->line++] = xstrdup(buf);
    582 }
    583 
    584 static int
    585 file_read_line(struct file *file)
    586 {
    587 	if (fgets(file->buf, sizeof(file->buf), file->fp) != NULL) {
    588 		file->buf[strcspn(file->buf, "\n")] = '\0';
    589 		return (1);
    590 	}
    591 
    592 	if (ferror(file->fp))
    593 		fatal("I/O error while reading '%s'", file->path);
    594 
    595 	/* assumes EOF. */
    596 	return (0);
    597 }
    598 
    599 static void
    600 file_consume_newline(struct file *file)
    601 {
    602 	if (!file_read_line(file))
    603 		fatal("expected newline, got eof in '%s'", file->path);
    604 
    605 	if (file->buf[0] != '\0') {
    606 		fatal("expected newline, got '%s' in '%s'",
    607 		    file->buf, file->path);
    608 	}
    609 }
    610 
    611 static void
    612 file_parse(struct context *ctx, const char *path)
    613 {
    614 	size_t			idx;
    615 	struct file		file;
    616 	const char		*func;
    617 	struct entry		*entry;
    618 	struct topic		*topic;
    619 	char			*id, *name, *p, *s;
    620 	int			len, indent, pos, order;
    621 	u_int8_t		digest[SHA256_DIGEST_LENGTH];
    622 
    623 	file_open(&file, path, "r");
    624 
    625 	while (file_read_line(&file)) {
    626 		file_cache_line(&file, file.buf);
    627 
    628 		if ((p = strstr(file.buf, TAG_OPEN)) == NULL)
    629 			continue;
    630 
    631 		func = NULL;
    632 		p += sizeof(TAG_OPEN) - 1;
    633 
    634 		if (file.line > 0) {
    635 			pos = file.line - 1;
    636 			while (pos >= 0) {
    637 				if (file.type == FILE_TYPE_PYTHON &&
    638 				    ((s = strstr(file.lc[pos], "def ")))) {
    639 					func = s + sizeof("def ") - 1;
    640 					break;
    641 				}
    642 				if (isalpha(*(unsigned char *)file.lc[pos]) ||
    643 				    file.lc[pos][0] == '_') {
    644 					func = file.lc[pos];
    645 					break;
    646 				}
    647 				pos--;
    648 			}
    649 		}
    650 
    651 		if (note_parse_arguments(p, &order, &name, &id) == -1) {
    652 			fprintf(stderr, "skipping malformed secnote in %s:%d\n",
    653 			    file.path, file.line);
    654 			continue;
    655 		}
    656 
    657 		topic = topic_resolve(ctx, name);
    658 		entry = topic_record_entry(ctx, topic, id, path, func, order);
    659 		if (entry == NULL) {
    660 			fprintf(stderr, "skipping duplicate senote in %s:%d\n",
    661 			    file.path, file.line);
    662 			continue;
    663 		}
    664 
    665 		indent = -1;
    666 		entry->line_start = file.line + 1;
    667 
    668 		for (;;) {
    669 			if (!file_read_line(&file))
    670 				fatal("EOF in '%s' before end section", path);
    671 
    672 			file_cache_line(&file, file.buf);
    673 
    674 			if (strstr(file.buf, TAG_CLOSE))
    675 				break;
    676 
    677 			p = file.buf;
    678 
    679 			if (indent == -1) {
    680 				indent = 0;
    681 
    682 				while (*p == '\t') {
    683 					p++;
    684 					indent++;
    685 				}
    686 
    687 				if (*p != '\t' && indent > 0)
    688 					p--;
    689 			} else {
    690 				if (strlen(p) > (size_t)indent - 1)
    691 					p += indent - 1;
    692 			}
    693 
    694 			entry_record_line(entry, p);
    695 		}
    696 
    697 		if (!SHA256_Final(digest, &entry->shactx))
    698 			fatal("failed to calculate digest");
    699 
    700 		for (idx = 0; idx < sizeof(digest); idx++) {
    701 			len = snprintf(entry->digest + (idx * 2),
    702 			    sizeof(entry->digest) - (idx * 2), "%02x",
    703 			    digest[idx]);
    704 			if (len == -1 || (size_t)len >= sizeof(entry->digest))
    705 				fatal("failed to create hex digest");
    706 		}
    707 
    708 		entry->line_end = file.line - 1;
    709 	}
    710 
    711 	file_close(&file);
    712 }
    713 
    714 static int
    715 note_parse_arguments(char *note, int *order, char **topic, char **id)
    716 {
    717 	const char	*errstr;
    718 	int		idx, count;
    719 	char		*v, *args[5], **ap, **ptr;
    720 
    721 	*id = NULL;
    722 	*order = -1;
    723 	*topic = NULL;
    724 
    725 	count = 0;
    726 	for (ap = args; ap < &args[5] &&
    727 	    (*ap = strsep(&note, " ")) != NULL;) {
    728 		if (**ap != '\0') {
    729 			ap++;
    730 			count++;
    731 		}
    732 	}
    733 
    734 	for (idx = 0; idx < count; idx++) {
    735 		v = NULL;
    736 		ptr = NULL;
    737 
    738 		if (!strncmp(args[idx], "topic=", sizeof("topic=") - 1))
    739 			ptr = topic;
    740 
    741 		if (!strncmp(args[idx], "id=", sizeof("id=") - 1))
    742 			ptr = id;
    743 
    744 		if (ptr == NULL)
    745 			continue;
    746 
    747 		if ((v = strchr(args[idx], '=')) == NULL)
    748 			fatal("failure to find '=' unexpected");
    749 
    750 		*(v)++ = '\0';
    751 		*ptr = v;
    752 	}
    753 
    754 	if (*topic == NULL || *id == NULL)
    755 		return (-1);
    756 
    757 	if ((v = strchr(*topic, ':')) != NULL) {
    758 		*(v)++ = '\0';
    759 
    760 		errstr = NULL;
    761 		*order = strtonum(v, 0, USHRT_MAX, &errstr);
    762 		if (errstr != NULL)
    763 			return (-1);
    764 	}
    765 
    766 	return (0);
    767 }
    768 
    769 static struct topic *
    770 topic_resolve(struct context *ctx, const char *name)
    771 {
    772 	struct topic		*topic;
    773 
    774 	topic = NULL;
    775 
    776 	TAILQ_FOREACH(topic, &ctx->topics, list) {
    777 		if (!strcasecmp(topic->name, name))
    778 			break;
    779 	}
    780 
    781 	if (topic == NULL) {
    782 		if ((topic = calloc(1, sizeof(*topic))) == NULL)
    783 			fatal("%s: calloc", __func__);
    784 
    785 		topic->name = xstrdup(name);
    786 		TAILQ_INIT(&topic->entries);
    787 
    788 		TAILQ_INSERT_TAIL(&ctx->topics, topic, list);
    789 	}
    790 
    791 	return (topic);
    792 }
    793 
    794 static void
    795 topic_free(struct context *ctx, struct topic *topic)
    796 {
    797 	struct entry	*entry;
    798 
    799 	TAILQ_REMOVE(&ctx->topics, topic, list);
    800 
    801 	while ((entry = TAILQ_FIRST(&topic->entries)) != NULL) {
    802 		TAILQ_REMOVE(&topic->entries, entry, list);
    803 		topic_entry_free(entry);
    804 	}
    805 
    806 	free(topic->name);
    807 	free(topic);
    808 }
    809 
    810 static void
    811 topic_entry_free(struct entry *entry)
    812 {
    813 	struct line	*line;
    814 
    815 	while ((line = TAILQ_FIRST(&entry->lines)) != NULL) {
    816 		TAILQ_REMOVE(&entry->lines, line, list);
    817 		free(line->code);
    818 		free(line);
    819 	}
    820 
    821 	free(entry->context);
    822 	free(entry->id);
    823 	free(entry->file);
    824 	free(entry);
    825 }
    826 
    827 static struct entry *
    828 topic_record_entry(struct context *ctx, struct topic *topic, const char *id,
    829     const char *file, const char *context, int order)
    830 {
    831 	int			strip;
    832 	const char		*p, *s;
    833 	struct entry		*entry, *ent;
    834 
    835 	TAILQ_FOREACH(entry, &topic->entries, list) {
    836 		if (!strcmp(entry->id, id)) {
    837 			fprintf(stderr,
    838 			    "duplicate id '%s' in file %s for topic '%s', ",
    839 			    id, file, topic->name);
    840 			fprintf(stderr, "previously used in file %s:%d\n",
    841 			    entry->file, entry->line_start);
    842 			return (NULL);
    843 		}
    844 	}
    845 
    846 	if ((entry = calloc(1, sizeof(*entry))) == NULL)
    847 		fatal("%s: calloc failed", __func__);
    848 
    849 	p = file;
    850 	strip = ctx->pnum;
    851 
    852 	while (strip != 0 && p != NULL) {
    853 		p = strchr(p, '/');
    854 		if (p != NULL)
    855 			p = p + 1;
    856 		strip--;
    857 	}
    858 
    859 	if (p == NULL)
    860 		fatal("-p%d makes no sense with '%s'", ctx->pnum, file);
    861 
    862 	entry->id = xstrdup(id);
    863 	entry->file = xstrdup(p);
    864 
    865 	if (context) {
    866 		s = context;
    867 		while (isspace(*(const unsigned char *)s))
    868 			s++;
    869 
    870 		if ((p = strchr(s, '(')) == NULL)
    871 			p = s + strlen(s);
    872 
    873 		if ((entry->context = strndup(s, p - s)) == NULL)
    874 			fatal("%s: strdup failed", __func__);
    875 	}
    876 
    877 	entry->order = order;
    878 
    879 	if (!SHA256_Init(&entry->shactx))
    880 		fatal("failed to initialise SHA256 context");
    881 
    882 	ent = NULL;
    883 	TAILQ_INIT(&entry->lines);
    884 
    885 	if (entry->order != -1) {
    886 		TAILQ_FOREACH(ent, &topic->entries, list) {
    887 			if (ent->order > entry->order) {
    888 				TAILQ_INSERT_BEFORE(ent, entry, list);
    889 				break;
    890 			}
    891 		}
    892 	}
    893 
    894 	if (ent == NULL)
    895 		TAILQ_INSERT_TAIL(&topic->entries, entry, list);
    896 
    897 	return (entry);
    898 }
    899 
    900 static void
    901 entry_record_line(struct entry *entry, const char *code)
    902 {
    903 	struct line	*line;
    904 
    905 	if ((line = calloc(1, sizeof(*line))) == NULL)
    906 		fatal("%s: calloc", __func__);
    907 
    908 	line->code = xstrdup(code);
    909 
    910 	if (!SHA256_Update(&entry->shactx, code, strlen(code)))
    911 		fatal("failed to update digest");
    912 
    913 	TAILQ_INSERT_TAIL(&entry->lines, line, list);
    914 }
    915 
    916 static int
    917 entry_check_state(struct entry_list *head, struct entry *orig,
    918     struct entry **out)
    919 {
    920 	int		state;
    921 	struct entry	*entry;
    922 
    923 	*out = NULL;
    924 	state = ENTRY_STATE_GONE;
    925 
    926 	TAILQ_FOREACH(entry, head, list) {
    927 		if (strcmp(orig->file, entry->file))
    928 			continue;
    929 
    930 		/* @secnote-open topic=note-matching id=match-id */
    931 		/*
    932 		 * Attemp to the match the ID of the note to resolve it.
    933 		 * If it does not match but we see the note is otherwise
    934 		 * the same, we mark it as renamed.
    935 		 */
    936 		if (strcmp(orig->id, entry->id)) {
    937 			if (orig->line_start == entry->line_start &&
    938 			    orig->line_end == entry->line_end &&
    939 			    !strcmp(orig->digest, entry->digest)) {
    940 				*out = entry;
    941 				return (ENTRY_STATE_RENAMED);
    942 			}
    943 
    944 			continue;
    945 		}
    946 		/* @secnote-close */
    947 
    948 		state = 0;
    949 		*out = entry;
    950 
    951 		/* @secnote-open topic=note-matching id=match-position */
    952 		/*
    953 		 * If the note moved start or end line it was considered
    954 		 * moved from the original note.
    955 		 */
    956 		if (orig->line_start != entry->line_start ||
    957 		    orig->line_end != entry->line_end)
    958 			state |= ENTRY_STATE_MOVED;
    959 		/* @secnote-close */
    960 
    961 		/* @secnote-open topic=note-matching id=match-digest */
    962 		/*
    963 		 * Finally if the digest matches the original note its
    964 		 * digest we know it has not changed contents.
    965 		 */
    966 		if (!strcmp(entry->digest, orig->digest))
    967 			state |= ENTRY_STATE_SAME;
    968 		else
    969 			state |= ENTRY_STATE_DIFFERS;
    970 		/* @secnote-close */
    971 
    972 		break;
    973 	}
    974 
    975 	if (state == ENTRY_STATE_GONE)
    976 		*out = NULL;
    977 
    978 	return (state);
    979 }
    980 
    981 static int
    982 text_chunk_new_entries(struct topic *topic, int *header)
    983 {
    984 	int		new;
    985 	struct entry	*entry;
    986 
    987 	new = 0;
    988 
    989 	TAILQ_FOREACH(entry, &topic->entries, list) {
    990 		if (*header == 0) {
    991 			*header = 1;
    992 			printf("%s\n", FILE_SEPARATOR);
    993 			printf("%s\n", topic->name);
    994 			printf("%s\n\n", FILE_SEPARATOR);
    995 		}
    996 
    997 		new++;
    998 		printf("    %s in %s:%d-%d\n      - new\n", entry->id,
    999 		    entry->file, entry->line_start, entry->line_end);
   1000 	}
   1001 
   1002 	return (new);
   1003 }
   1004 
   1005 static void
   1006 text_topic_dump(struct context *ctx)
   1007 {
   1008 	struct topic		*topic, *next;
   1009 
   1010 	for (topic = TAILQ_FIRST(&ctx->topics); topic != NULL; topic = next) {
   1011 		next = TAILQ_NEXT(topic, list);
   1012 
   1013 		if (ctx->list) {
   1014 			if (ctx->full)
   1015 				text_topic_write(ctx, topic);
   1016 			else
   1017 				text_topic_header(ctx, topic);
   1018 		} else {
   1019 			if (ctx->query == NULL ||
   1020 			    fnmatch(ctx->query, topic->name, FNM_NOESCAPE) == 0)
   1021 				text_topic_write(ctx, topic);
   1022 		}
   1023 
   1024 		topic_free(ctx, topic);
   1025 	}
   1026 }
   1027 
   1028 static void
   1029 text_topic_header(struct context *ctx, struct topic *topic)
   1030 {
   1031 	if (!ctx->list || ctx->full)
   1032 		printf("@ ");
   1033 
   1034 	printf("%s", topic->name);
   1035 
   1036 	printf("\n");
   1037 }
   1038 
   1039 static void
   1040 text_topic_write(struct context *ctx, struct topic *topic)
   1041 {
   1042 	struct line		*line;
   1043 	const char		*last;
   1044 	struct entry		*entry;
   1045 
   1046 	text_topic_header(ctx, topic);
   1047 
   1048 	if (!ctx->list || ctx->full)
   1049 		printf("\n");
   1050 
   1051 	last = NULL;
   1052 
   1053 	TAILQ_FOREACH(entry, &topic->entries, list) {
   1054 		if (ctx->list) {
   1055 			if (ctx->db)
   1056 				printf("%s:", entry->digest);
   1057 			printf("%s:%s:%d-%d", entry->id, entry->file,
   1058 			    entry->line_start, entry->line_end);
   1059 			if (entry->context)
   1060 				printf(":%s", entry->context);
   1061 			printf("\n");
   1062 			continue;
   1063 		}
   1064 
   1065 		if (last == NULL || strcmp(last, entry->file)) {
   1066 			printf("File: %s\n", entry->file);
   1067 			printf("%s\n", FILE_SEPARATOR);
   1068 			last = entry->file;
   1069 		}
   1070 
   1071 		printf("@@ %s %d-%d @@ ", entry->id,
   1072 		    entry->line_start, entry->line_end);
   1073 
   1074 		if (entry->context)
   1075 			printf("%s ", entry->context);
   1076 
   1077 		if (entry->order != -1)
   1078 			printf("(%d)\n", entry->order);
   1079 		else
   1080 			printf("\n");
   1081 
   1082 		TAILQ_FOREACH(line, &entry->lines, list)
   1083 			printf("%s\n", line->code);
   1084 
   1085 		printf("\n");
   1086 	}
   1087 
   1088 	if (ctx->list)
   1089 		printf("\n");
   1090 }
   1091 
   1092 static int
   1093 filecmp(const FTSENT **a1, const FTSENT **b1)
   1094 {
   1095 	const FTSENT	*a = *a1;
   1096 	const FTSENT	*b = *b1;
   1097 
   1098 	return (strcmp(a->fts_name, b->fts_name));
   1099 }
   1100 
   1101 static char *
   1102 xstrdup(const char *str)
   1103 {
   1104 	char	*ptr;
   1105 
   1106 	if ((ptr = strdup(str)) == NULL)
   1107 		fatal("strdup: %s", strerror(errno));
   1108 
   1109 	return (ptr);
   1110 }
   1111 
   1112 static void
   1113 fatal(const char *fmt, ...)
   1114 {
   1115 	va_list		args;
   1116 
   1117 	va_start(args, fmt);
   1118 	vfprintf(stderr, fmt, args);
   1119 	va_end(args);
   1120 
   1121 	fprintf(stderr, "\n");
   1122 	exit(1);
   1123 }