]> git.ipfire.org Git - thirdparty/wireguard-tools.git/commitdiff
contrib: introduce simple highlighter library
authorJason A. Donenfeld <Jason@zx2c4.com>
Sat, 5 Jan 2019 16:02:14 +0000 (17:02 +0100)
committerJason A. Donenfeld <Jason@zx2c4.com>
Wed, 23 Jan 2019 13:29:44 +0000 (14:29 +0100)
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
contrib/highlighter/Makefile [new file with mode: 0644]
contrib/highlighter/README [new file with mode: 0644]
contrib/highlighter/fuzz.c [new file with mode: 0644]
contrib/highlighter/gui/highlight.cpp [new file with mode: 0644]
contrib/highlighter/gui/highlight.pro [new file with mode: 0644]
contrib/highlighter/highlight.c [new file with mode: 0644]
contrib/highlighter/highlighter.c [new file with mode: 0644]
contrib/highlighter/highlighter.h [new file with mode: 0644]

diff --git a/contrib/highlighter/Makefile b/contrib/highlighter/Makefile
new file mode 100644 (file)
index 0000000..29e1402
--- /dev/null
@@ -0,0 +1,25 @@
+CFLAGS ?= -O3 -march=native
+CFLAGS += -std=gnu99
+CFLAGS += -Wall
+CFLAGS += -MMD -MP
+
+highlight: highlight.o highlighter.o
+
+fuzz: CC := clang
+fuzz: CFLAGS += -fsanitize=fuzzer
+fuzz: fuzz.c highlighter.c
+
+gui/Makefile: gui/highlight.pro
+       cd gui && qmake
+gui: gui/Makefile
+       @$(MAKE) -C gui
+
+clean:
+       rm -f highlight fuzz *.o *.d
+       @if [ -f gui/Makefile ]; then $(MAKE) -C gui distclean; fi
+
+.PHONY: clean gui
+.DEFAULT_GOAL: highlight
+MAKEFLAGS += --no-print-directory
+
+-include *.d
diff --git a/contrib/highlighter/README b/contrib/highlighter/README
new file mode 100644 (file)
index 0000000..2ea5141
--- /dev/null
@@ -0,0 +1,22 @@
+wg(8) and wg-quick(8) syntax highlighter library
+================================================
+
+highlighter.c contains a simple portable highlighter for the wg(8) and
+wg-quick(8) configuration files. Simply copy `highlight.c` and
+`highlight.h` into your project wholesale.
+
+As a demo, a simple console highlighter program is included, alongside a
+simple Qt5 GUI app to show its usage in realtime.
+
+There is also a basic fuzzer, because why not?
+
+Usage:
+
+    $ make
+    $ ./highlight < path/to/tunnel.conf
+
+    $ make gui
+    $ ./gui/highlight
+
+    $ make fuzz
+    $ ./fuzz -workers=$(nproc) -jobs=$(nproc) ./corpus
diff --git a/contrib/highlighter/fuzz.c b/contrib/highlighter/fuzz.c
new file mode 100644 (file)
index 0000000..e308157
--- /dev/null
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "highlighter.h"
+
+int LLVMFuzzerTestOneInput(const char *data, size_t size)
+{
+       char *str = strndup(data, size);
+       if (!str)
+               return 0;
+       struct highlight_span *spans = highlight_config(str);
+       if (!spans)
+               return 0;
+       for (struct highlight_span *span = spans; span->type != HighlightEnd; ++span);
+       free(spans);
+       free(str);
+       return 0;
+}
diff --git a/contrib/highlighter/gui/highlight.cpp b/contrib/highlighter/gui/highlight.cpp
new file mode 100644 (file)
index 0000000..a95857b
--- /dev/null
@@ -0,0 +1,90 @@
+#include <QApplication>
+#include <QPlainTextEdit>
+#include <QPalette>
+#include <QFontDatabase>
+#include <QVBoxLayout>
+#include <QPushButton>
+
+extern "C" {
+#include "../highlighter.h"
+}
+
+static QColor colormap[] = {
+       [HighlightSection] = QColor("#ababab"),
+       [HighlightField] = QColor("#70c0b1"),
+       [HighlightPrivateKey] = QColor("#7aa6da"),
+       [HighlightPublicKey] = QColor("#7aa6da"),
+       [HighlightPresharedKey] = QColor("#7aa6da"),
+       [HighlightIP] = QColor("#b9ca4a"),
+       [HighlightCidr] = QColor("#e78c45"),
+       [HighlightHost] = QColor("#b9ca4a"),
+       [HighlightPort] = QColor("#e78c45"),
+       [HighlightMTU] = QColor("#c397d8"),
+       [HighlightKeepalive] = QColor("#c397d8"),
+       [HighlightComment] = QColor("#969896"),
+       [HighlightDelimiter] = QColor("#7aa6da"),
+#ifndef MOBILE_WGQUICK_SUBSET
+       [HighlightTable] = QColor("#c397d8"),
+       [HighlightFwMark] = QColor("#c397d8"),
+       [HighlightSaveConfig] = QColor("#c397d8"),
+       [HighlightCmd] = QColor("#969896"),
+#endif
+       [HighlightError] = QColor("#d54e53")
+};
+
+int main(int argc, char *argv[])
+{
+       QApplication a(argc, argv);
+       QWidget w;
+       w.setWindowTitle(QObject::tr("WireGuard Configuration Highlighter"));
+       QVBoxLayout v;
+       w.setLayout(&v);
+       QPlainTextEdit e;
+       v.addWidget(&e);
+       QPalette p(e.palette());
+       p.setColor(QPalette::Base, QColor("#010101"));
+       p.setColor(QPalette::Text, QColor("#eaeaea"));
+       e.setPalette(p);
+       QFont f(QFontDatabase::systemFont(QFontDatabase::FixedFont));
+       f.setPointSize(16);
+       e.setFont(f);
+       e.setMinimumSize(400, 500);
+       bool guard = false;
+       QObject::connect(&e, &QPlainTextEdit::textChanged, [&]() {
+               if (guard)
+                       return;
+               struct highlight_span *spans = highlight_config(e.toPlainText().toLatin1().data());
+               if (!spans)
+                       return;
+               QTextCursor cursor(e.document());
+               QTextCharFormat format;
+               cursor.beginEditBlock();
+               cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
+               cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
+               format.setForeground(p.color(QPalette::Text));
+               format.setUnderlineStyle(QTextCharFormat::NoUnderline);
+               cursor.mergeCharFormat(format);
+               for (struct highlight_span *span = spans; span->type != HighlightEnd; ++span) {
+                       cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
+                       cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, span->start);
+                       cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, span->len);
+                       format.setForeground(colormap[span->type]);
+                       format.setUnderlineStyle(span->type == HighlightError ? QTextCharFormat::SpellCheckUnderline : QTextCharFormat::NoUnderline);
+                       cursor.mergeCharFormat(format);
+               }
+               free(spans);
+               guard = true;
+               cursor.endEditBlock();
+               guard = false;
+       });
+       QPushButton b;
+       v.addWidget(&b);
+       b.setText(QObject::tr("&Randomize colors"));
+       QObject::connect(&b, &QPushButton::clicked, [&]() {
+               for (size_t i = 0; i < sizeof(colormap) / sizeof(colormap[0]); ++i)
+                       colormap[i] = QColor::fromHsl(qrand() % 360, qrand() % 192 + 64, qrand() % 128 + 128);
+               e.setPlainText(e.toPlainText());
+       });
+       w.show();
+       return a.exec();
+}
diff --git a/contrib/highlighter/gui/highlight.pro b/contrib/highlighter/gui/highlight.pro
new file mode 100644 (file)
index 0000000..f25667c
--- /dev/null
@@ -0,0 +1,5 @@
+QT += core gui widgets
+TEMPLATE = app
+TARGET = highlight
+SOURCES += highlight.cpp ../highlighter.c
+HEADERS += ../highlighter.h
diff --git a/contrib/highlighter/highlight.c b/contrib/highlighter/highlight.c
new file mode 100644 (file)
index 0000000..b03f792
--- /dev/null
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "highlighter.h"
+
+#define TERMINAL_FG_BLACK      "\x1b[30m"
+#define TERMINAL_FG_RED                "\x1b[31m"
+#define TERMINAL_FG_GREEN      "\x1b[32m"
+#define TERMINAL_FG_YELLOW     "\x1b[33m"
+#define TERMINAL_FG_BLUE       "\x1b[34m"
+#define TERMINAL_FG_MAGENTA    "\x1b[35m"
+#define TERMINAL_FG_CYAN       "\x1b[36m"
+#define TERMINAL_FG_WHITE      "\x1b[37m"
+#define TERMINAL_FG_DEFAULT    "\x1b[39m"
+
+#define TERMINAL_BG_BLACK      "\x1b[40m"
+#define TERMINAL_BG_RED                "\x1b[41m"
+#define TERMINAL_BG_GREEN      "\x1b[42m"
+#define TERMINAL_BG_YELLOW     "\x1b[43m"
+#define TERMINAL_BG_BLUE       "\x1b[44m"
+#define TERMINAL_BG_MAGENTA    "\x1b[45m"
+#define TERMINAL_BG_CYAN       "\x1b[46m"
+#define TERMINAL_BG_WHITE      "\x1b[47m"
+#define TERMINAL_BG_DEFAULT    "\x1b[49m"
+
+#define TERMINAL_BOLD          "\x1b[1m"
+#define TERMINAL_NO_BOLD       "\x1b[22m"
+#define TERMINAL_UNDERLINE     "\x1b[4m"
+#define TERMINAL_NO_UNDERLINE  "\x1b[24m"
+
+#define TERMINAL_RESET         "\x1b[0m"
+
+static const char *colormap[] = {
+       [HighlightSection] = TERMINAL_FG_BLACK TERMINAL_BOLD,
+       [HighlightField] = TERMINAL_FG_BLUE TERMINAL_BOLD,
+       [HighlightPrivateKey] = TERMINAL_FG_YELLOW TERMINAL_BOLD,
+       [HighlightPublicKey] = TERMINAL_FG_YELLOW TERMINAL_BOLD,
+       [HighlightPresharedKey] = TERMINAL_FG_YELLOW TERMINAL_BOLD,
+       [HighlightIP] = TERMINAL_FG_GREEN,
+       [HighlightCidr] = TERMINAL_FG_YELLOW,
+       [HighlightHost] = TERMINAL_FG_GREEN TERMINAL_BOLD,
+       [HighlightPort] = TERMINAL_FG_MAGENTA,
+       [HighlightMTU] = TERMINAL_FG_BLUE,
+       [HighlightKeepalive] = TERMINAL_FG_BLUE,
+       [HighlightComment] = TERMINAL_FG_CYAN,
+       [HighlightDelimiter] = TERMINAL_FG_CYAN,
+#ifndef MOBILE_WGQUICK_SUBSET
+       [HighlightTable] = TERMINAL_FG_BLUE,
+       [HighlightFwMark] = TERMINAL_FG_BLUE,
+       [HighlightSaveConfig] = TERMINAL_FG_BLUE,
+       [HighlightCmd] = TERMINAL_FG_WHITE,
+#endif
+       [HighlightError] = TERMINAL_FG_RED TERMINAL_UNDERLINE
+};
+
+int main(int argc, char *argv[])
+{
+       char input[1024 * 1024];
+       struct highlight_span *spans;
+       size_t last = 0, total_len;
+
+       total_len = fread(input, 1, sizeof(input) - 1, stdin);
+       input[total_len] = '\0';
+       spans = highlight_config(input);
+
+       fputs(TERMINAL_RESET, stdout);
+       for (struct highlight_span *span = spans; span->type != HighlightEnd; ++span) {
+               fwrite(input + last, 1, span->start - last, stdout);
+               fputs(colormap[span->type], stdout);
+               fwrite(input + span->start, 1, span->len, stdout);
+               fputs(TERMINAL_RESET, stdout);
+               last = span->start + span->len;
+       }
+       fwrite(input + last, 1, total_len - last, stdout);
+
+       free(spans);
+       return 0;
+}
diff --git a/contrib/highlighter/highlighter.c b/contrib/highlighter/highlighter.c
new file mode 100644 (file)
index 0000000..171a84c
--- /dev/null
@@ -0,0 +1,620 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "highlighter.h"
+
+typedef struct {
+       const char *s;
+       size_t len;
+} string_span_t;
+
+static bool is_decimal(char c)
+{
+       return c >= '0' && c <= '9';
+}
+
+static bool is_hexadecimal(char c)
+{
+       return is_decimal(c) || ((c | 32) >= 'a' && (c | 32) <= 'f');
+}
+
+static bool is_alphabet(char c)
+{
+       return (c | 32) >= 'a' && (c | 32) <= 'z';
+}
+
+static bool is_same(string_span_t s, const char *c)
+{
+       size_t len = strlen(c);
+
+       if (len != s.len)
+               return false;
+       return !memcmp(s.s, c, len);
+}
+
+static bool is_caseless_same(string_span_t s, const char *c)
+{
+       size_t len = strlen(c);
+
+       if (len != s.len)
+               return false;
+       for (size_t i = 0; i < len; ++i) {
+               char a = c[i], b = s.s[i];
+               if ((unsigned)a - 'a' < 26)
+                       a &= 95;
+               if ((unsigned)b - 'a' < 26)
+                       b &= 95;
+               if (a != b)
+                       return false;
+       }
+       return true;
+}
+
+static bool is_valid_key(string_span_t s)
+{
+       if (s.len != 44 || s.s[43] != '=')
+               return false;
+
+       for (size_t i = 0; i < 43; ++i) {
+               if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
+                   s.s[i] != '/' && s.s[i] != '+')
+                       return false;
+       }
+       return true;
+}
+
+static bool is_valid_hostname(string_span_t s)
+{
+       size_t num_digit = 0, num_entity = s.len;
+
+       if (s.len > 63 || !s.len)
+               return false;
+       if (s.s[0] == '-' || s.s[s.len - 1] == '-')
+               return false;
+       if (s.s[0] == '.' || s.s[s.len - 1] == '.')
+               return false;
+
+       for (size_t i = 0; i < s.len; ++i) {
+               if (is_decimal(s.s[i])) {
+                       ++num_digit;
+                       continue;
+               }
+               if (s.s[i] == '.') {
+                       --num_entity;
+                       continue;
+               }
+
+               if (!is_alphabet(s.s[i]) && s.s[i] != '-')
+                       return false;
+
+               if (i && s.s[i] == '.' && s.s[i - 1] == '.')
+                       return false;
+       }
+       return num_digit != num_entity;
+}
+
+static bool is_valid_ipv4(string_span_t s)
+{
+       for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
+               uint32_t val = 0;
+
+               for (j = 0; j < 3 && pos + j < s.len && is_decimal(s.s[pos + j]); ++j)
+                       val = 10 * val + s.s[pos + j] - '0';
+               if (j == 0 || (j > 1 && s.s[pos] == '0') || val > 255)
+                       return false;
+               if (pos + j == s.len && i == 3)
+                       return true;
+               if (s.s[pos + j] != '.')
+                       return false;
+               pos += j + 1;
+       }
+       return false;
+}
+
+static bool is_valid_ipv6(string_span_t s)
+{
+       size_t pos = 0;
+       bool seen_colon = false;
+
+       if (s.len < 2)
+               return false;
+       if (s.s[pos] == ':' && s.s[++pos] != ':')
+               return false;
+       if (s.s[s.len - 1] == ':' && s.s[s.len - 2] != ':')
+               return false;
+
+       for (size_t j, i = 0; pos < s.len; ++i) {
+               if (s.s[pos] == ':' && !seen_colon) {
+                       seen_colon = true;
+                       if (++pos == s.len)
+                               break;
+                       if (i == 7)
+                               return false;
+                       continue;
+               }
+               for (j = 0; j < 4 && pos + j < s.len && is_hexadecimal(s.s[pos + j]); ++j);
+               if (j == 0)
+                       return false;
+               if (pos + j == s.len && (seen_colon || i == 7))
+                       break;
+               if (i == 7)
+                       return false;
+               if (s.s[pos + j] != ':') {
+                       if (s.s[pos + j] != '.' || (i < 6 && !seen_colon))
+                               return false;
+                       return is_valid_ipv4((string_span_t){ s.s + pos, s.len - pos });
+               }
+               pos += j + 1;
+       }
+       return true;
+}
+
+static bool is_valid_uint(string_span_t s, bool support_hex, uint64_t min, uint64_t max)
+{
+       uint64_t val = 0;
+
+       /* Bound this around 32 bits, so that we don't have to write overflow logic. */
+       if (s.len > 10 || !s.len)
+               return false;
+
+       if (support_hex && s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
+               for (size_t i = 2; i < s.len; ++i) {
+                       if (s.s[i] - '0' < 10)
+                               val = 16 * val + (s.s[i] - '0');
+                       else if ((s.s[i] | 32) - 'a' < 6)
+                               val = 16 * val + (s.s[i] | 32) - 'a' + 10;
+                       else
+                               return false;
+               }
+       } else {
+               for (size_t i = 0; i < s.len; ++i) {
+                       if (!is_decimal(s.s[i]))
+                               return false;
+                       val = 10 * val + s.s[i] - '0';
+               }
+       }
+       return val <= max && val >= min;
+}
+
+static bool is_valid_port(string_span_t s)
+{
+       return is_valid_uint(s, false, 0, 65535);
+}
+
+static bool is_valid_mtu(string_span_t s)
+{
+       return is_valid_uint(s, false, 576, 65535);
+}
+
+static bool is_valid_persistentkeepalive(string_span_t s)
+{
+       if (is_same(s, "off"))
+               return true;
+       return is_valid_uint(s, false, 0, 65535);
+}
+
+#ifndef MOBILE_WGQUICK_SUBSET
+
+static bool is_valid_fwmark(string_span_t s)
+{
+       if (is_same(s, "off"))
+               return true;
+       return is_valid_uint(s, true, 0, 4294967295);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+       if (is_same(s, "auto"))
+               return true;
+       if (is_same(s, "off"))
+               return true;
+       /* This pretty much invalidates the other checks, but rt_names.c's
+        * fread_id_name does no validation aside from this. */
+       if (s.len < 512)
+               return true;
+       return is_valid_uint(s, false, 0, 4294967295);
+}
+
+static bool is_valid_saveconfig(string_span_t s)
+{
+       return is_same(s, "true") || is_same(s, "false");
+}
+
+static bool is_valid_prepostupdown(string_span_t s)
+{
+       /* It's probably not worthwhile to try to validate a bash expression.
+        * So instead we just demand non-zero length. */
+       return s.len;
+}
+#endif
+
+static bool is_valid_scope(string_span_t s)
+{
+       if (s.len > 64 || !s.len)
+               return false;
+       for (size_t i = 0; i < s.len; ++i) {
+               if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+                   s.s[i] != '_' && s.s[i] != '=' && s.s[i] != '+' &&
+                   s.s[i] != '.' && s.s[i] != '-')
+                       return false;
+       }
+       return true;
+}
+
+static bool is_valid_endpoint(string_span_t s)
+{
+
+       if (!s.len)
+               return false;
+
+       if (s.s[0] == '[') {
+               bool seen_scope = false;
+               string_span_t hostspan = { s.s + 1, 0 };
+
+               for (size_t i = 1; i < s.len; ++i) {
+                       if (s.s[i] == '%') {
+                               if (seen_scope)
+                                       return false;
+                               seen_scope = true;
+                               if (!is_valid_ipv6(hostspan))
+                                       return false;
+                               hostspan = (string_span_t){ s.s + i + 1, 0 };
+                       } else if (s.s[i] == ']') {
+                               if (seen_scope) {
+                                       if (!is_valid_scope(hostspan))
+                                               return false;
+                               } else if (!is_valid_ipv6(hostspan)) {
+                                       return false;
+                               }
+                               if (i == s.len - 1 || s.s[i + 1] != ':')
+                                       return false;
+                               return is_valid_port((string_span_t){ s.s + i + 2, s.len - i - 2 });
+                       } else {
+                               ++hostspan.len;
+                       }
+               }
+               return false;
+       }
+       for (size_t i = 0; i < s.len; ++i) {
+               if (s.s[i] == ':') {
+                       string_span_t host = { s.s, i }, port = { s.s + i + 1, s.len - i - 1};
+                       return is_valid_port(port) && (is_valid_ipv4(host) || is_valid_hostname(host));
+               }
+       }
+       return false;
+}
+
+static bool is_valid_network(string_span_t s)
+{
+       for (size_t i = 0; i < s.len; ++i) {
+               if (s.s[i] == '/') {
+                       string_span_t ip = { s.s, i }, cidr = { s.s + i + 1, s.len - i - 1};
+                       uint16_t cidrval = 0;
+
+                       if (cidr.len > 3 || !cidr.len)
+                               return false;
+
+                       for (size_t j = 0; j < cidr.len; ++j) {
+                               if (!is_decimal(cidr.s[j]))
+                                       return false;
+                               cidrval = 10 * cidrval + cidr.s[j] - '0';
+                       }
+                       if (is_valid_ipv4(ip))
+                               return cidrval <= 32;
+                       else if (is_valid_ipv6(ip))
+                               return cidrval <= 128;
+                       return false;
+               }
+       }
+       return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+static bool is_valid_dns(string_span_t s)
+{
+       return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+enum field {
+       InterfaceSection,
+       PrivateKey,
+       ListenPort,
+       Address,
+       DNS,
+       MTU,
+#ifndef MOBILE_WGQUICK_SUBSET
+       FwMark,
+       Table,
+       PreUp, PostUp, PreDown, PostDown,
+       SaveConfig,
+#endif
+
+       PeerSection,
+       PublicKey,
+       PresharedKey,
+       AllowedIPs,
+       Endpoint,
+       PersistentKeepalive,
+
+       Invalid
+};
+
+static enum field section_for_field(enum field t)
+{
+       if (t > InterfaceSection && t < PeerSection)
+               return InterfaceSection;
+       if (t > PeerSection && t < Invalid)
+               return PeerSection;
+       return Invalid;
+}
+
+static enum field get_field(string_span_t s)
+{
+#define check_enum(t) do { if (is_caseless_same(s, #t)) return t; } while (0)
+       check_enum(PrivateKey);
+       check_enum(ListenPort);
+       check_enum(Address);
+       check_enum(DNS);
+       check_enum(MTU);
+       check_enum(PublicKey);
+       check_enum(PresharedKey);
+       check_enum(AllowedIPs);
+       check_enum(Endpoint);
+       check_enum(PersistentKeepalive);
+#ifndef MOBILE_WGQUICK_SUBSET
+       check_enum(FwMark);
+       check_enum(Table);
+       check_enum(PreUp);
+       check_enum(PostUp);
+       check_enum(PreDown);
+       check_enum(PostDown);
+       check_enum(SaveConfig);
+#endif
+       return Invalid;
+#undef check_enum
+}
+
+static enum field get_sectiontype(string_span_t s)
+{
+       if (is_caseless_same(s, "[Peer]"))
+               return PeerSection;
+       if (is_caseless_same(s, "[Interface]"))
+               return InterfaceSection;
+       return Invalid;
+}
+
+struct highlight_span_array {
+       size_t len, capacity;
+       struct highlight_span *spans;
+};
+
+/* A useful OpenBSD-ism. */
+static void *realloc_array(void *optr, size_t nmemb, size_t size)
+{
+       if ((nmemb >= (size_t)1 << (sizeof(size_t) * 4) ||
+            size >= (size_t)1 << (sizeof(size_t) * 4)) &&
+           nmemb > 0 && SIZE_MAX / nmemb < size) {
+               errno = ENOMEM;
+               return NULL;
+       }
+       return realloc(optr, size * nmemb);
+}
+
+static bool append_highlight_span(struct highlight_span_array *a, const char *o, string_span_t s, enum highlight_type t)
+{
+       if (!s.len)
+               return true;
+       if (a->len >= a->capacity) {
+               struct highlight_span *resized;
+
+               a->capacity = a->capacity ? a->capacity * 2 : 64;
+               resized = realloc_array(a->spans, a->capacity, sizeof(*resized));
+               if (!resized) {
+                       free(a->spans);
+                       memset(a, 0, sizeof(*a));
+                       return false;
+               }
+               a->spans = resized;
+       }
+       a->spans[a->len++] = (struct highlight_span){ t, s.s - o, s.len };
+       return true;
+}
+
+static void highlight_multivalue_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+       switch (section) {
+       case DNS:
+               append_highlight_span(ret, parent.s, s, is_valid_dns(s) ? HighlightIP : HighlightError);
+               break;
+       case Address:
+       case AllowedIPs: {
+               size_t slash;
+
+               if (!is_valid_network(s)) {
+                       append_highlight_span(ret, parent.s, s, HighlightError);
+                       break;
+               }
+               for (slash = 0; slash < s.len; ++slash) {
+                       if (s.s[slash] == '/')
+                               break;
+               }
+               if (slash == s.len) {
+                       append_highlight_span(ret, parent.s, s, HighlightIP);
+               } else {
+                       append_highlight_span(ret, parent.s, (string_span_t){ s.s, slash }, HighlightIP);
+                       append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash, 1 }, HighlightDelimiter);
+                       append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash + 1, s.len - slash - 1 }, HighlightCidr);
+               }
+               break;
+       }
+       default:
+               append_highlight_span(ret, parent.s, s, HighlightError);
+       }
+}
+
+static void highlight_multivalue(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+       string_span_t current_span = { s.s, 0 };
+       size_t len_at_last_space = 0;
+
+       for (size_t i = 0; i < s.len; ++i) {
+               if (s.s[i] == ',') {
+                       current_span.len = len_at_last_space;
+                       highlight_multivalue_value(ret, parent, current_span, section);
+                       append_highlight_span(ret, parent.s, (string_span_t){ s.s + i, 1 }, HighlightDelimiter);
+                       len_at_last_space = 0;
+                       current_span = (string_span_t){ s.s + i + 1, 0 };
+               } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+                       if (&s.s[i] == current_span.s && !current_span.len)
+                               ++current_span.s;
+                       else
+                               ++current_span.len;
+               } else {
+                       len_at_last_space = ++current_span.len;
+               }
+       }
+       current_span.len = len_at_last_space;
+       if (current_span.len)
+               highlight_multivalue_value(ret, parent, current_span, section);
+       else if (ret->spans[ret->len - 1].type == HighlightDelimiter)
+               ret->spans[ret->len - 1].type = HighlightError;
+}
+
+static void highlight_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+       switch (section) {
+       case PrivateKey:
+               append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPrivateKey : HighlightError);
+               break;
+       case PublicKey:
+               append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPublicKey : HighlightError);
+               break;
+       case PresharedKey:
+               append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPresharedKey : HighlightError);
+               break;
+       case MTU:
+               append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
+               break;
+#ifndef MOBILE_WGQUICK_SUBSET
+       case SaveConfig:
+               append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
+               break;
+       case FwMark:
+               append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
+               break;
+       case Table:
+               append_highlight_span(ret, parent.s, s, is_valid_table(s) ? HighlightTable : HighlightError);
+               break;
+       case PreUp:
+       case PostUp:
+       case PreDown:
+       case PostDown:
+               append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
+               break;
+#endif
+       case ListenPort:
+               append_highlight_span(ret, parent.s, s, is_valid_port(s) ? HighlightPort : HighlightError);
+               break;
+       case PersistentKeepalive:
+               append_highlight_span(ret, parent.s, s, is_valid_persistentkeepalive(s) ? HighlightKeepalive : HighlightError);
+               break;
+       case Endpoint: {
+               size_t colon;
+
+               if (!is_valid_endpoint(s)) {
+                       append_highlight_span(ret, parent.s, s, HighlightError);
+                       break;
+               }
+               for (colon = s.len; colon --> 0;) {
+                       if (s.s[colon] == ':')
+                               break;
+               }
+               append_highlight_span(ret, parent.s, (string_span_t){ s.s, colon }, HighlightHost);
+               append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon, 1 }, HighlightDelimiter);
+               append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon + 1, s.len - colon - 1 }, HighlightPort);
+               break;
+       }
+       case Address:
+       case DNS:
+       case AllowedIPs:
+               highlight_multivalue(ret, parent, s, section);
+               break;
+       default:
+               append_highlight_span(ret, parent.s, s, HighlightError);
+       }
+}
+
+struct highlight_span *highlight_config(const char *config)
+{
+       struct highlight_span_array ret = { 0 };
+       const string_span_t s = { config, strlen(config) };
+       string_span_t current_span = { s.s, 0 };
+       enum field current_section = Invalid, current_field = Invalid;
+       enum { OnNone, OnKey, OnValue, OnComment, OnSection } state = OnNone;
+       size_t len_at_last_space = 0, equals_location = 0;
+
+       for (size_t i = 0; i <= s.len; ++i) {
+               if (i == s.len || s.s[i] == '\n' || (state != OnComment && s.s[i] == '#')) {
+                       if (state == OnKey) {
+                               current_span.len = len_at_last_space;
+                               append_highlight_span(&ret, s.s, current_span, HighlightError);
+                       } else if (state == OnValue) {
+                               if (current_span.len) {
+                                       append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightDelimiter);
+                                       current_span.len = len_at_last_space;
+                                       highlight_value(&ret, s, current_span, current_field);
+                               } else {
+                                       append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightError);
+                               }
+                       } else if (state == OnSection) {
+                               current_span.len = len_at_last_space;
+                               current_section = get_sectiontype(current_span);
+                               append_highlight_span(&ret, s.s, current_span, current_section == Invalid ? HighlightError : HighlightSection);
+                       } else if (state == OnComment) {
+                               append_highlight_span(&ret, s.s, current_span, HighlightComment);
+                       }
+                       if (i == s.len)
+                               break;
+                       len_at_last_space = 0;
+                       current_field = Invalid;
+                       if (s.s[i] == '#') {
+                               current_span = (string_span_t){ s.s + i, 1 };
+                               state = OnComment;
+                       } else {
+                               current_span = (string_span_t){ s.s + i + 1, 0 };
+                               state = OnNone;
+                       }
+               } else if (state == OnComment) {
+                       ++current_span.len;
+               } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+                       if (&s.s[i] == current_span.s && !current_span.len)
+                               ++current_span.s;
+                       else
+                               ++current_span.len;
+               } else if (s.s[i] == '=' && state == OnKey) {
+                       current_span.len = len_at_last_space;
+                       current_field = get_field(current_span);
+                       enum field section = section_for_field(current_field);
+                       if (section == Invalid || current_field == Invalid || section != current_section)
+                               append_highlight_span(&ret, s.s, current_span, HighlightError);
+                       else
+                               append_highlight_span(&ret, s.s, current_span, HighlightField);
+                       equals_location = i;
+                       current_span = (string_span_t){ s.s + i + 1, 0 };
+                       state = OnValue;
+               } else {
+                       if (state == OnNone)
+                               state = s.s[i] == '[' ? OnSection : OnKey;
+                       len_at_last_space = ++current_span.len;
+               }
+       }
+
+       append_highlight_span(&ret, s.s, (string_span_t){ s.s, -1 }, HighlightEnd);
+       return ret.spans;
+}
diff --git a/contrib/highlighter/highlighter.h b/contrib/highlighter/highlighter.h
new file mode 100644 (file)
index 0000000..c004e12
--- /dev/null
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <sys/types.h>
+
+enum highlight_type {
+       HighlightSection,
+       HighlightField,
+       HighlightPrivateKey,
+       HighlightPublicKey,
+       HighlightPresharedKey,
+       HighlightIP,
+       HighlightCidr,
+       HighlightHost,
+       HighlightPort,
+       HighlightMTU,
+       HighlightKeepalive,
+       HighlightComment,
+       HighlightDelimiter,
+#ifndef MOBILE_WGQUICK_SUBSET
+       HighlightTable,
+       HighlightFwMark,
+       HighlightSaveConfig,
+       HighlightCmd,
+#endif
+       HighlightError,
+       HighlightEnd
+};
+
+struct highlight_span {
+       enum highlight_type type;
+       size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);