secnote

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

commit 50634f5eb679314162b6225be9e44157143ee327
Author: Joris Vink <joris@coders.se>
Date:   Wed,  1 Jun 2022 22:53:28 +0200

Hello world secnote

Diffstat:
Makefile | 38++++++++++++++++++++++++++++++++++++++
secnote.c | 901+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 939 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,38 @@ +# secnote Makefile + +BIN=secnote +CC?=cc +PREFIX?=/usr/local +INSTALL_DIR=$(PREFIX)/bin +MAN_DIR?=$(PREFIX)/share/man + +SRC= secnote.c + +CFLAGS+=-Wall -Wextra -Werror -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+=-Wmissing-declarations -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+=-Wsign-compare -std=c99 -pedantic +CFLAGS+=-fsanitize=address -fstack-protector-all + +CFLAGS+=$(shell pkg-config openssl --cflags) + +LDFLAGS+=-fsanitize=address +LDFLAGS+=$(shell pkg-config openssl --libs) + +OBJS= $(SRC:%.c=%.o) + +all: $(BIN) + +$(BIN): $(OBJS) + $(CC) $(OBJS) $(LDFLAGS) -o $(BIN) + +install: + mkdir -p $(INSTALL_DIR) + install -m 555 $(BIN) $(INSTALL_DIR)/$(BIN) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -rf $(BIN) $(OBJS) + +.PHONY: all clean diff --git a/secnote.c b/secnote.c @@ -0,0 +1,901 @@ +/* + * Copyright (c) 2022 Joris Vink <joris@coders.se> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/queue.h> + +#include <openssl/sha.h> + +#include <ctype.h> +#include <errno.h> +#include <fnmatch.h> +#include <fts.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define DUMP_PARSE_TOPIC 1 +#define DUMP_PARSE_ENTRY 2 + +#define ENTRY_STATE_MOVED (1 << 1) +#define ENTRY_STATE_DIFFERS (1 << 2) +#define ENTRY_STATE_GONE (1 << 3) +#define ENTRY_STATE_SAME (1 << 4) +#define ENTRY_STATE_GREW (1 << 5) +#define ENTRY_STATE_SHRUNK (1 << 6) + +#define FILE_SEPARATOR \ + "===================================================================" + +#define TAG_OPEN "@secnote-open" +#define TAG_CLOSE "@secnote-close" + +#define MAX(a, b) ((a > b) ? a : b) +#define MIN(a, b) ((a < b) ? a : b) + +struct line { + char *code; + TAILQ_ENTRY(line) list; +}; + +struct entry { + int order; + + char *file; + char *code; + char *context; + + int line_start; + int line_end; + + SHA256_CTX shactx; + char digest[(SHA256_DIGEST_LENGTH * 2) + 1]; + + TAILQ_HEAD(, line) lines; + TAILQ_ENTRY(entry) list; +}; + +TAILQ_HEAD(entry_list, entry); + +struct topic { + char *name; + struct entry_list entries; + TAILQ_ENTRY(topic) list; +}; + +struct file { + FILE *fp; + char *path; +}; + +struct context { + int list; + int pnum; + int full; + int digest; + const char *query; + + struct topic *topic; + TAILQ_HEAD(, topic) topics; +}; + +static void fatal(const char *, ...); +static int filecmp(const FTSENT **, const FTSENT **); + +static void entry_record_line(struct entry *, const char *); +static int entry_check_state(struct entry_list *, struct entry *, + struct entry **); + +static void file_close(struct file *); +static void file_consume_newline(struct file *); +static void file_parse(struct context *, const char *); +static int file_read_line(struct file *, char *, size_t); +static void file_open(struct file *, const char *, const char *); + +static void text_topic_write(struct context *, struct topic *); +static void text_topic_header(struct context *, struct topic *); + +static void load_from_dump(struct context *, const char *); +static void load_from_args(struct context *, int, char **); + +static int dump_parse_entry(struct context *, struct file *, char *); +static int dump_parse_topic(struct context *, struct file *, const char *); + +static void text_topic_dump(struct context *); +static void topic_free(struct context *, struct topic *); +static void context_compare(struct context *, struct context *); + +static struct topic *topic_resolve(struct context *, const char *); +static struct entry *topic_record_entry(struct context *, struct topic *, + const char *, const char *, int); + +static void +usage(void) +{ + fprintf(stderr, + "Usage: secnote [-pnum] [-l [-f] | -d | -q query] [src]\n"); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + int ch; + struct context ctx, vfy; + const char *err, *verify; + + verify = NULL; + + memset(&ctx, 0, sizeof(ctx)); + memset(&vfy, 0, sizeof(vfy)); + + TAILQ_INIT(&ctx.topics); + TAILQ_INIT(&vfy.topics); + + while ((ch = getopt(argc, argv, "dfhlp:q:v:")) != -1) { + switch (ch) { + case 'd': + ctx.list = 1; + ctx.full = 1; + ctx.digest = 1; + break; + case 'f': + ctx.full = 1; + break; + case 'l': + ctx.list = 1; + break; + case 'p': + ctx.pnum = strtonum(optarg, 0, 255, &err); + if (err != NULL) + fatal("-p %s invalid: %s", optarg, err); + break; + case 'q': + ctx.query = optarg; + break; + case 'v': + verify = optarg; + break; + case 'h': + default: + usage(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) + usage(); + + if (ctx.list && (ctx.query || verify)) + fatal("-l/-d and -q/-v are mutually exclusive"); + + if (ctx.full && !ctx.list) { + fprintf(stderr, "-f only works with -l\n"); + usage(); + } + + load_from_args(&ctx, argc, argv); + + if (verify) { + load_from_dump(&vfy, verify); + context_compare(&vfy, &ctx); + } else { + if (TAILQ_EMPTY(&ctx.topics)) + printf("no topics found\n"); + else + text_topic_dump(&ctx); + } + + return (0); +} + +static void +context_compare(struct context *verify, struct context *ondisk) +{ + const char *sep; + struct topic *t1, *t2; + struct entry *entry, *ent; + int a, b, changes, header, state; + + changes = 0; + + TAILQ_FOREACH(t1, &verify->topics, list) { + TAILQ_FOREACH(t2, &ondisk->topics, list) { + if (!strcmp(t2->name, t1->name)) + break; + } + + if (t2 == NULL) { + changes++; + printf("topic '%s' not found in source\n", t1->name); + continue; + } + + header = 0; + + TAILQ_FOREACH(entry, &t1->entries, list) { + state = entry_check_state(&t2->entries, entry, &ent); + + if (state != ENTRY_STATE_SAME && !header) { + header = 1; + printf("@ %s\n\n", t1->name); + } + + switch (state) { + case ENTRY_STATE_SAME: + continue; + case ENTRY_STATE_GONE: + changes++; + printf("chunk '%s' (%d-%d) not found\n", + entry->file, entry->line_start, + entry->line_end); + continue; + } + + changes++; + sep = NULL; + + printf("chunk '%s' (%d-%d) ", entry->file, + entry->line_start, entry->line_end); + + a = entry->line_end - entry->line_start; + b = ent->line_end - ent->line_start; + + if (a < b) + state |= ENTRY_STATE_GREW; + else if (a > b) + state |= ENTRY_STATE_SHRUNK; + + if (state & ENTRY_STATE_MOVED) { + sep = ", "; + printf("moved %d-%d", + ent->line_start, ent->line_end); + } + + if (state & ENTRY_STATE_DIFFERS) { + if (sep) + printf("%s", sep); + + printf("modified"); + + if (state & + (ENTRY_STATE_SHRUNK | ENTRY_STATE_GREW)) + printf(" %+d lines(s)", b - a); + } + + printf("\n"); + } + + if (header) + printf("\n"); + } + + if (changes > 0) + fatal("%d change%s detected", changes, changes > 1 ? "s" : ""); + + printf("secnote verified\n"); +} + +static void +load_from_args(struct context *ctx, int argc, char **argv) +{ + struct stat st; + int idx; + FTS *fts; + FTSENT *ent; + char *pv[2], *ext; + + for (idx = 0; idx < argc; idx++) { + if (stat(argv[idx], &st) == -1 || + access(argv[idx], R_OK) == -1) { + fprintf(stderr, "skipping '%s' (%s)\n", + argv[idx], strerror(errno)); + continue; + } + + if (S_ISREG(st.st_mode)) { + file_parse(ctx, argv[idx]); + continue; + } + + if (!S_ISDIR(st.st_mode)) { + fprintf(stderr, "skipping '%s'\n", argv[idx]); + continue; + } + + pv[0] = argv[idx]; + pv[1] = NULL; + + fts = fts_open(pv, + FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, filecmp); + if (fts == NULL) + fatal("fts_open: %s", strerror(errno)); + + while ((ent = fts_read(fts)) != NULL) { + if (!S_ISREG(ent->fts_statp->st_mode)) + continue; + + if ((ext = strrchr(ent->fts_name, '.')) == NULL) + continue; + + if (strcmp(ext, ".c") && strcmp(ext, ".h")) + continue; + + file_parse(ctx, ent->fts_path); + } + + fts_close(fts); + } +} + +static void +load_from_dump(struct context *ctx, const char *path) +{ + struct file file; + int state; + char buf[512]; + + if (!strcmp(path, "-")) { + file.fp = stdin; + file.path = "<stdin>"; + } else { + file_open(&file, path, "r"); + } + + state = DUMP_PARSE_TOPIC; + + while (file_read_line(&file, buf, sizeof(buf))) { + switch (state) { + case DUMP_PARSE_TOPIC: + state = dump_parse_topic(ctx, &file, buf); + break; + case DUMP_PARSE_ENTRY: + state = dump_parse_entry(ctx, &file, buf); + break; + default: + fatal("invalid parse state %d", state); + } + } + + if (file.fp != stdin) + file_close(&file); +} + +static int +dump_parse_topic(struct context *ctx, struct file *file, const char *line) +{ + if (line[0] != '@') + fatal("expected start of topic, got '%s'", line); + + ctx->topic = topic_resolve(ctx, &line[2]); + file_consume_newline(file); + + return (DUMP_PARSE_ENTRY); +} + +static int +dump_parse_entry(struct context *ctx, struct file *file, char *line) +{ + int count; + struct entry *entry; + char **ap, *args[5], *hash, *path, *region, *context; + + (void)ctx; + (void)file; + + if (line[0] == '\0') { + ctx->topic = NULL; + return (DUMP_PARSE_TOPIC); + } + + count = 0; + for (ap = args; ap < &args[5] && + (*ap = strsep(&line, ":")) != NULL;) { + if (**ap != '\0') { + ap++; + count++; + } + } + + if (count != 3 && count != 4) + fatal("invalid entry in file '%s'", file->path); + + hash = args[0]; + path = args[1]; + region = args[2]; + + if (count > 3) + context = args[3]; + else + context = NULL; + + entry = topic_record_entry(ctx, ctx->topic, path, context, -1); + + if (strlcpy(entry->digest, hash, sizeof(entry->digest)) >= + sizeof(entry->digest)) + fatal("invalid hash string '%s' in '%s'", hash, file->path); + + if (sscanf(region, "%d-%d", &entry->line_start, &entry->line_end) != 2) + fatal("invalid region string '%s' in '%s'", region, file->path); + + return (DUMP_PARSE_ENTRY); +} + +static void +file_open(struct file *file, const char *path, const char *mode) +{ + memset(file, 0, sizeof(*file)); + + if ((file->fp = fopen(path, mode)) == NULL) + fatal("fopen(%s): %s", path, strerror(errno)); + + if ((file->path = strdup(path)) == NULL) + fatal("strdup: %s", strerror(errno)); +} + +static void +file_close(struct file *file) +{ + fclose(file->fp); + free(file->path); +} + +static int +file_read_line(struct file *file, char *buf, size_t len) +{ + if (fgets(buf, len, file->fp) != NULL) { + buf[strcspn(buf, "\n")] = '\0'; + return (1); + } + + if (ferror(file->fp)) + fatal("I/O error while reading '%s'", file->path); + + /* assumes EOF. */ + return (0); +} + +static void +file_consume_newline(struct file *file) +{ + char buf[512]; + + if (!file_read_line(file, buf, sizeof(buf))) + fatal("expected newline, got eof in '%s'", file->path); + + if (buf[0] != '\0') + fatal("expected newline, got '%s' in '%s'", buf, file->path); +} + +static void +file_parse(struct context *ctx, const char *path) +{ + struct file file; + struct entry *entry; + struct topic *topic; + size_t newsz, idx; + const char *context, *errstr; + u_int8_t digest[SHA256_DIGEST_LENGTH]; + char buf[512], name[64], *p, **lc; + int len, indent, pos, line, order; + + file_open(&file, path, "r"); + + line = 0; + lc = NULL; + + while (file_read_line(&file, buf, sizeof(buf))) { + newsz = sizeof(char *) * (line + 1); + if ((lc = realloc(lc, newsz)) == NULL) + fatal("realloc(%zu): %s", newsz, strerror(errno)); + + if ((lc[line++] = strdup(buf)) == NULL) + fatal("strdup: %s", strerror(errno)); + + if ((p = strstr(buf, TAG_OPEN)) == NULL) + continue; + + context = NULL; + if (line > 0) { + pos = line - 1; + while (pos >= 0) { + if (isalpha(*(unsigned char *)lc[pos]) || + lc[pos][0] == '_') { + context = lc[pos]; + break; + } + pos--; + } + } + + p += sizeof(TAG_OPEN) - 1; + memset(name, 0, sizeof(name)); + + if (sscanf(p, " topic=%63s", name) != 1) { + fprintf(stderr, "malformed %s in %s:%d\n", + TAG_OPEN, path, line); + continue; + } + + order = -1; + if ((p = strchr(name, ':')) != NULL) { + *(p)++ = '\0'; + + errstr = NULL; + order = strtonum(p, 0, USHRT_MAX, &errstr); + if (errstr != NULL) { + fprintf(stderr, "malformed topic in %s:%d\n", + path, line); + continue; + } + } + + topic = topic_resolve(ctx, name); + entry = topic_record_entry(ctx, topic, path, context, order); + + indent = -1; + entry->line_start = line + 1; + + for (;;) { + if (!file_read_line(&file, buf, sizeof(buf))) + fatal("EOF in '%s' before end section", path); + + newsz = sizeof(char *) * (line + 1); + if ((lc = realloc(lc, newsz)) == NULL) { + fatal("realloc(%zu): %s", newsz, + strerror(errno)); + } + + if ((lc[line++] = strdup(buf)) == NULL) + fatal("strdup: %s", strerror(errno)); + + if (strstr(buf, TAG_CLOSE)) + break; + + p = buf; + + if (indent == -1) { + indent = 0; + + while (*p == '\t') { + p++; + indent++; + } + + if (*p != '\t' && indent > 0) + p--; + } else { + if (strlen(p) > (size_t)indent - 1) + p += indent - 1; + } + + entry_record_line(entry, p); + } + + if (!SHA256_Final(digest, &entry->shactx)) + fatal("failed to calculate digest"); + + for (idx = 0; idx < sizeof(digest); idx++) { + len = snprintf(entry->digest + (idx * 2), + sizeof(entry->digest) - (idx * 2), "%02x", + digest[idx]); + if (len == -1 || (size_t)len >= sizeof(entry->digest)) + fatal("failed to create hex digest"); + } + + entry->line_end = line - 1; + } + + line = line - 1; + + while (line >= 0) { + free(lc[line]); + line--; + } + + free(lc); + file_close(&file); +} + +static struct topic * +topic_resolve(struct context *ctx, const char *name) +{ + struct topic *topic; + + topic = NULL; + + TAILQ_FOREACH(topic, &ctx->topics, list) { + if (!strcasecmp(topic->name, name)) + break; + } + + if (topic == NULL) { + if ((topic = calloc(1, sizeof(*topic))) == NULL) + fatal("%s: calloc", __func__); + + if ((topic->name = strdup(name)) == NULL) + fatal("%s: strdup", __func__); + + TAILQ_INIT(&topic->entries); + TAILQ_INSERT_TAIL(&ctx->topics, topic, list); + } + + return (topic); +} + +static void +topic_free(struct context *ctx, struct topic *topic) +{ + struct line *line, *lnext; + struct entry *entry, *enext; + + TAILQ_REMOVE(&ctx->topics, topic, list); + + for (entry = TAILQ_FIRST(&topic->entries); entry != NULL; + entry = enext) { + enext = TAILQ_NEXT(entry, list); + TAILQ_REMOVE(&topic->entries, entry, list); + + for (line = TAILQ_FIRST(&entry->lines); line != NULL; + line = lnext) { + lnext = TAILQ_NEXT(line, list); + TAILQ_REMOVE(&entry->lines, line, list); + + free(line->code); + free(line); + } + + free(entry->context); + free(entry->file); + free(entry); + } + + free(topic->name); + free(topic); +} + +static struct entry * +topic_record_entry(struct context *ctx, struct topic *topic, const char *file, + const char *context, int order) +{ + const char *p; + int strip; + struct entry *entry, *ent; + + if ((entry = calloc(1, sizeof(*entry))) == NULL) + fatal("%s: calloc failed", __func__); + + p = file; + strip = ctx->pnum; + + while (strip != 0 && p != NULL) { + p = strchr(p, '/'); + if (p != NULL) + p = p + 1; + strip--; + } + + if (p == NULL) + fatal("-p%d doesn't work on '%s'", ctx->pnum, file); + + if ((entry->file = strdup(p)) == NULL) + fatal("%s: strdup failed", __func__); + + entry->order = order; + + if (!SHA256_Init(&entry->shactx)) + fatal("failed to initialise SHA256 context"); + + if (context) { + if ((p = strchr(context, '(')) == NULL) + p = context + strlen(context); + + if ((entry->context = strndup(context, p - context)) == NULL) + fatal("%s: strdup failed", __func__); + } + + ent = NULL; + TAILQ_INIT(&entry->lines); + + if (entry->order != -1) { + TAILQ_FOREACH(ent, &topic->entries, list) { + if (ent->order > entry->order) { + TAILQ_INSERT_BEFORE(ent, entry, list); + break; + } + } + } + + if (ent == NULL) + TAILQ_INSERT_TAIL(&topic->entries, entry, list); + + return (entry); +} + +static void +entry_record_line(struct entry *entry, const char *code) +{ + struct line *line; + + if ((line = calloc(1, sizeof(*line))) == NULL) + fatal("%s: calloc", __func__); + + if ((line->code = strdup(code)) == NULL) + fatal("%s: strdup", __func__); + + if (!SHA256_Update(&entry->shactx, code, strlen(code))) + fatal("failed to update digest"); + + TAILQ_INSERT_TAIL(&entry->lines, line, list); +} + +static int +entry_check_state(struct entry_list *head, struct entry *orig, + struct entry **out) +{ + struct entry *entry; + int a, b, state; + + TAILQ_FOREACH(entry, head, list) { + if (strcmp(orig->file, entry->file)) + continue; + + *out = entry; + + a = MAX(orig->line_start, entry->line_start); + b = MIN(orig->line_end, entry->line_end); + + if (a <= b) { + if (!strcmp(entry->digest, orig->digest)) { + state = ENTRY_STATE_SAME; + if (orig->line_start != entry->line_start) + return (state | ENTRY_STATE_MOVED); + return (state); + } + + if (orig->line_start == entry->line_start) + return (ENTRY_STATE_DIFFERS); + + return (ENTRY_STATE_DIFFERS | ENTRY_STATE_MOVED); + } + + if (!strcmp(entry->digest, orig->digest)) + return (ENTRY_STATE_SAME | ENTRY_STATE_MOVED); + } + + *out = NULL; + + return (ENTRY_STATE_GONE); +} + +static void +text_topic_dump(struct context *ctx) +{ + struct topic *topic, *next; + + for (topic = TAILQ_FIRST(&ctx->topics); topic != NULL; topic = next) { + next = TAILQ_NEXT(topic, list); + + if (ctx->list) { + if (ctx->full) + text_topic_write(ctx, topic); + else + text_topic_header(ctx, topic); + + continue; + } + + if (ctx->query == NULL || + fnmatch(ctx->query, topic->name, FNM_NOESCAPE) == 0) + text_topic_write(ctx, topic); + + topic_free(ctx, topic); + } +} + +static void +text_topic_header(struct context *ctx, struct topic *topic) +{ + if (!ctx->list || ctx->full) + printf("@ "); + + printf("%s", topic->name); + + printf("\n"); +} + +static void +text_topic_write(struct context *ctx, struct topic *topic) +{ + struct line *line; + const char *last; + struct entry *entry; + + text_topic_header(ctx, topic); + + if (!ctx->list || ctx->full) + printf("\n"); + + last = NULL; + + TAILQ_FOREACH(entry, &topic->entries, list) { + if (ctx->list) { + if (ctx->digest) + printf("%s:", entry->digest); + printf("%s:%d-%d", entry->file, + entry->line_start, entry->line_end); + if (entry->context) + printf(":%s", entry->context); + printf("\n"); + continue; + } + + if (last == NULL || strcmp(last, entry->file)) { + printf("File: %s\n", entry->file); + printf("%s\n", FILE_SEPARATOR); + last = entry->file; + } + + printf("@@ %d-%d @@", entry->line_start, entry->line_end); + + if (entry->context) + printf(" %s ", entry->context); + else + printf(" "); + + if (entry->order != -1) + printf("(%d)\n", entry->order); + else + printf("\n"); + + TAILQ_FOREACH(line, &entry->lines, list) + printf("%s\n", line->code); + + printf("\n"); + } + + if (ctx->list) + printf("\n"); +} + +static int +filecmp(const FTSENT **a1, const FTSENT **b1) +{ + const FTSENT *a = *a1; + const FTSENT *b = *b1; + + return (strcmp(a->fts_name, b->fts_name)); +} + +static void +fatal(const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + + fprintf(stderr, "\n"); + exit(1); +}