aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md9
-rw-r--r--cbinding.go27
-rw-r--r--compare_test.go52
-rw-r--r--corpus/demo.conf10
-rw-r--r--corpus/slantedandenchanted.conf15
-rw-r--r--go.mod3
-rw-r--r--highlighter.c608
-rw-r--r--highlighter.go631
-rw-r--r--highlighter.h33
9 files changed, 1388 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8351ce3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+## Translation of `highlighter.c` into Go
+
+This repo contains `highlighter.c` and `highlighter.go`. The latter is a translation of the former. As a result of being translated from C, it contains two functions -- `append` and `at` -- that make use of unsafe operations. Additionally, the entry function appends a NULL byte, which results in an unfortunate allocation. It does work, however, and is decently fast.
+
+In order to test equivalence, there's a simple fuzz test, runnable with `go test -v -fuzz FuzzComparison` using Go ≥1.18.
+
+The goal here is to get rid of the unsafe operations in `highlighter.go` and to get rid of the NULL byte addition.
+
+It would be nice to do this with as few changes as possible. For example, functions like `isCaselessSame` or `isValidIPv6` may have similar functions in the standard library, but those might possibly behave differently in subtle ways. Rather than trying to rewrite _everything_, it is preferable to simply work on replacing the bespoke representation of strings and string slices, in order to remove the unsafe operations and get rid of the NULL byte addition. Additionally, keeping the structure close to the original C implementation makes it easier to maintain moving forward.
diff --git a/cbinding.go b/cbinding.go
new file mode 100644
index 0000000..7866539
--- /dev/null
+++ b/cbinding.go
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
+ */
+
+package syntax
+
+// #include "highlighter.h"
+// #include <stdlib.h>
+import "C"
+import (
+ "unsafe"
+)
+
+func highlightConfigC(config string) []highlightSpan {
+ b := append([]byte(config), 0)
+ highlights := C.highlight_config((*C.char)(unsafe.Pointer(&b[0])))
+ if highlights == nil {
+ return nil
+ }
+ var ret []highlightSpan
+ for i := highlights; i._type != C.HighlightEnd; i = (*C.struct_highlight_span)(unsafe.Add(unsafe.Pointer(i), unsafe.Sizeof(*i))) {
+ ret = append(ret, highlightSpan{highlight(i._type), int(i.start), int(i.len)})
+ }
+ C.free(unsafe.Pointer(highlights))
+ return ret
+}
diff --git a/compare_test.go b/compare_test.go
new file mode 100644
index 0000000..3922e2d
--- /dev/null
+++ b/compare_test.go
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
+ */
+
+package syntax
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func FuzzComparison(f *testing.F) {
+ corpus, err := filepath.Glob("./corpus/*.conf")
+ if err != nil {
+ f.Fatal(err)
+ }
+ for _, c := range corpus {
+ b, err := os.ReadFile(c)
+ if err != nil {
+ f.Fatal(err)
+ }
+ f.Logf("Adding %+q to corpus", c)
+ f.Add(string(b))
+ }
+ corpus, err = filepath.Glob("/etc/wireguard/*.conf")
+ if err == nil {
+ for _, c := range corpus {
+ b, err := os.ReadFile(c)
+ if err == nil {
+ f.Logf("Adding %+q to corpus", c)
+ f.Add(string(b))
+ }
+ }
+ }
+
+ f.Fuzz(func(t *testing.T, in string) {
+ in, _, _ = strings.Cut(in, "\x00")
+ goOutput := highlightConfig(in)
+ cOutput := highlightConfigC(in)
+ if len(goOutput) != len(cOutput) {
+ t.Fatalf("output length is not the same: %+v vs %+v: %+q", goOutput, cOutput, in)
+ }
+ for i := range goOutput {
+ if goOutput[i] != cOutput[i] {
+ t.Fatalf("struct return mismatch: %+v vs %+v: %+q", goOutput[i], cOutput[i], in)
+ }
+ }
+ })
+}
diff --git a/corpus/demo.conf b/corpus/demo.conf
new file mode 100644
index 0000000..8d35d28
--- /dev/null
+++ b/corpus/demo.conf
@@ -0,0 +1,10 @@
+[Interface]
+PrivateKey = 6OW3hqO7T1vMk+s//av6QZAA/fRgj2bKuDjTbOcSVkM=
+Address = 192.168.4.61/24
+DNS = 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1
+PostUp = calc.exe /humdum
+
+[Peer]
+PublicKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
+Endpoint = demo.wireguard.com:12912 # this is a comment
+AllowedIPs = 0.0.0.0/0
diff --git a/corpus/slantedandenchanted.conf b/corpus/slantedandenchanted.conf
new file mode 100644
index 0000000..fa3e1b0
--- /dev/null
+++ b/corpus/slantedandenchanted.conf
@@ -0,0 +1,15 @@
+[Interface]
+PrivateKey = 6OW3hqO7T1vMk+s//av6QZAA/fRgj2bKuDjTbOcSVkM=
+Address = 192.168.4.61/24, fff:d83::1.2.3.4/125
+DNS = aa::bbb, ::a
+Table = off
+
+[Peer]
+PresharedKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
+Endpoint = [abcd::1]:3883
+AllowedIPs = 0.0.0.0/0, ::/0
+
+[Peer]
+PrivateKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
+Endpoint = [abcd::1]:3883
+AllowedIPs = 1.282.2.4.2.32 # Invalid!
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..d6e390b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module syntax
+
+go 1.18
diff --git a/highlighter.c b/highlighter.c
new file mode 100644
index 0000000..4f08bf1
--- /dev/null
+++ b/highlighter.c
@@ -0,0 +1,608 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2020 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 < 42; ++i) {
+ if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
+ s.s[i] != '/' && s.s[i] != '+')
+ return false;
+ }
+ switch (s.s[42]) {
+ case 'A':
+ case 'E':
+ case 'I':
+ case 'M':
+ case 'Q':
+ case 'U':
+ case 'Y':
+ case 'c':
+ case 'g':
+ case 'k':
+ case 'o':
+ case 's':
+ case 'w':
+ case '4':
+ case '8':
+ case '0':
+ break;
+ default:
+ 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 ((unsigned)s.s[i] - '0' < 10)
+ val = 16 * val + (s.s[i] - '0');
+ else if (((unsigned)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);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+ if (is_same(s, "auto"))
+ return true;
+ if (is_same(s, "main"))
+ return true;
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, false, 0, 4294967295);
+}
+
+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;
+}
+
+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);
+}
+
+enum field {
+ InterfaceSection,
+ PrivateKey,
+ ListenPort,
+ Address,
+ DNS,
+ MTU,
+ Table,
+ PreUp, PostUp, PreDown, PostDown,
+
+ 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);
+ check_enum(Table);
+ check_enum(PreUp);
+ check_enum(PostUp);
+ check_enum(PreDown);
+ check_enum(PostDown);
+ 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:
+ if (is_valid_ipv4(s) || is_valid_ipv6(s))
+ append_highlight_span(ret, parent.s, s, HighlightIP);
+ else if (is_valid_hostname(s))
+ append_highlight_span(ret, parent.s, s, HighlightHost);
+ else
+ append_highlight_span(ret, parent.s, s, 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;
+ 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;
+ 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/highlighter.go b/highlighter.go
new file mode 100644
index 0000000..d8e6a2a
--- /dev/null
+++ b/highlighter.go
@@ -0,0 +1,631 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
+ *
+ * This is a direct translation of the original C, and for that reason, it's pretty unusual Go code:
+ * https://git.zx2c4.com/wireguard-tools/tree/contrib/highlighter/highlighter.c
+ */
+
+package syntax
+
+import "unsafe"
+
+type highlight int
+
+const (
+ highlightSection highlight = iota
+ highlightField
+ highlightPrivateKey
+ highlightPublicKey
+ highlightPresharedKey
+ highlightIP
+ highlightCidr
+ highlightHost
+ highlightPort
+ highlightMTU
+ highlightKeepalive
+ highlightComment
+ highlightDelimiter
+ highlightTable
+ highlightCmd
+ highlightError
+)
+
+func validateHighlight(isValid bool, t highlight) highlight {
+ if isValid {
+ return t
+ }
+ return highlightError
+}
+
+type highlightSpan struct {
+ t highlight
+ s int
+ len int
+}
+
+func isDecimal(c byte) bool {
+ return c >= '0' && c <= '9'
+}
+
+func isHexadecimal(c byte) bool {
+ return isDecimal(c) || (c|32) >= 'a' && (c|32) <= 'f'
+}
+
+func isAlphabet(c byte) bool {
+ return (c|32) >= 'a' && (c|32) <= 'z'
+}
+
+type stringSpan struct {
+ s *byte
+ len int
+}
+
+func (s stringSpan) at(i int) *byte {
+ return (*byte)(unsafe.Add(unsafe.Pointer(s.s), uintptr(i)))
+}
+
+func (s stringSpan) isSame(c string) bool {
+ if s.len != len(c) {
+ return false
+ }
+ cb := ([]byte)(c)
+ for i := 0; i < s.len; i++ {
+ if *s.at(i) != cb[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func (s stringSpan) isCaselessSame(c string) bool {
+ if s.len != len(c) {
+ return false
+ }
+ cb := ([]byte)(c)
+ for i := 0; i < s.len; i++ {
+ a := *s.at(i)
+ b := cb[i]
+ if a-'a' < 26 {
+ a &= 95
+ }
+ if b-'a' < 26 {
+ b &= 95
+ }
+ if a != b {
+ return false
+ }
+ }
+ return true
+}
+
+func (s stringSpan) isValidKey() bool {
+ if s.len != 44 || *s.at(43) != '=' {
+ return false
+ }
+ for i := 0; i < 42; i++ {
+ if !isDecimal(*s.at(i)) && !isAlphabet(*s.at(i)) && *s.at(i) != '/' && *s.at(i) != '+' {
+ return false
+ }
+ }
+ switch *s.at(42) {
+ case 'A', 'E', 'I', 'M', 'Q', 'U', 'Y', 'c', 'g', 'k', 'o', 's', 'w', '4', '8', '0':
+ return true
+ }
+ return false
+}
+
+func (s stringSpan) isValidHostname() bool {
+ numDigit := 0
+ numEntity := s.len
+ if s.len > 63 || s.len == 0 {
+ return false
+ }
+ if *s.s == '-' || *s.at(s.len - 1) == '-' {
+ return false
+ }
+ if *s.s == '.' || *s.at(s.len - 1) == '.' {
+ return false
+ }
+ for i := 0; i < s.len; i++ {
+ if isDecimal(*s.at(i)) {
+ numDigit++
+ continue
+ }
+ if *s.at(i) == '.' {
+ numEntity--
+ continue
+ }
+ if !isAlphabet(*s.at(i)) && *s.at(i) != '-' {
+ return false
+ }
+ if i != 0 && *s.at(i) == '.' && *s.at(i - 1) == '.' {
+ return false
+ }
+ }
+ return numDigit != numEntity
+}
+
+func (s stringSpan) isValidIPv4() bool {
+ pos := 0
+ for i := 0; i < 4 && pos < s.len; i++ {
+ val := 0
+ j := 0
+ for ; j < 3 && pos+j < s.len && isDecimal(*s.at(pos + j)); j++ {
+ val = 10*val + int(*s.at(pos + j)-'0')
+ }
+ if j == 0 || j > 1 && *s.at(pos) == '0' || val > 255 {
+ return false
+ }
+ if pos+j == s.len && i == 3 {
+ return true
+ }
+ if *s.at(pos + j) != '.' {
+ return false
+ }
+ pos += j + 1
+ }
+ return false
+}
+
+func (s stringSpan) isValidIPv6() bool {
+ if s.len < 2 {
+ return false
+ }
+ pos := 0
+ if *s.at(0) == ':' {
+ if *s.at(1) != ':' {
+ return false
+ }
+ pos = 1
+ }
+ if *s.at(s.len - 1) == ':' && *s.at(s.len - 2) != ':' {
+ return false
+ }
+ seenColon := false
+ for i := 0; pos < s.len; i++ {
+ if *s.at(pos) == ':' && !seenColon {
+ seenColon = true
+ pos++
+ if pos == s.len {
+ break
+ }
+ if i == 7 {
+ return false
+ }
+ continue
+ }
+ j := 0
+ for ; ; j++ {
+ if j < 4 && pos+j < s.len && isHexadecimal(*s.at(pos + j)) {
+ continue
+ }
+ break
+ }
+ if j == 0 {
+ return false
+ }
+ if pos+j == s.len && (seenColon || i == 7) {
+ break
+ }
+ if i == 7 {
+ return false
+ }
+ if *s.at(pos + j) != ':' {
+ if *s.at(pos + j) != '.' || i < 6 && !seenColon {
+ return false
+ }
+ return stringSpan{s.at(pos), s.len - pos}.isValidIPv4()
+ }
+ pos += j + 1
+ }
+ return true
+}
+
+func (s stringSpan) isValidUint(supportHex bool, min, max uint64) bool {
+ // Bound this around 32 bits, so that we don't have to write overflow logic.
+ if s.len > 10 || s.len == 0 {
+ return false
+ }
+ val := uint64(0)
+ if supportHex && s.len > 2 && *s.s == '0' && *s.at(1) == 'x' {
+ for i := 2; i < s.len; i++ {
+ if *s.at(i)-'0' < 10 {
+ val = 16*val + uint64(*s.at(i)-'0')
+ } else if (*s.at(i))|32-'a' < 6 {
+ val = 16*val + uint64((*s.at(i)|32)-'a'+10)
+ } else {
+ return false
+ }
+ }
+ } else {
+ for i := 0; i < s.len; i++ {
+ if !isDecimal(*s.at(i)) {
+ return false
+ }
+ val = 10*val + uint64(*s.at(i)-'0')
+ }
+ }
+ return val <= max && val >= min
+}
+
+func (s stringSpan) isValidPort() bool {
+ return s.isValidUint(false, 0, 65535)
+}
+
+func (s stringSpan) isValidMTU() bool {
+ return s.isValidUint(false, 576, 65535)
+}
+
+func (s stringSpan) isValidTable() bool {
+ return s.isSame("off") || s.isSame("auto") || s.isSame("main") || s.isValidUint(false, 0, (1<<32)-1)
+}
+
+func (s stringSpan) isValidPersistentKeepAlive() bool {
+ if s.isSame("off") {
+ return true
+ }
+ return s.isValidUint(false, 0, 65535)
+}
+
+// It's probably not worthwhile to try to validate a bash expression. So instead we just demand non-zero length.
+func (s stringSpan) isValidPrePostUpDown() bool {
+ return s.len != 0
+}
+
+func (s stringSpan) isValidScope() bool {
+ if s.len > 64 || s.len == 0 {
+ return false
+ }
+ for i := 0; i < s.len; i++ {
+ if isAlphabet(*s.at(i)) && !isDecimal(*s.at(i)) && *s.at(i) != '_' && *s.at(i) != '=' && *s.at(i) != '+' && *s.at(i) != '.' && *s.at(i) != '-' {
+ return false
+ }
+ }
+ return true
+}
+
+func (s stringSpan) isValidEndpoint() bool {
+ if s.len == 0 {
+ return false
+ }
+ if *s.s == '[' {
+ seenScope := false
+ hostspan := stringSpan{s.at(1), 0}
+ for i := 1; i < s.len; i++ {
+ if *s.at(i) == '%' {
+ if seenScope {
+ return false
+ }
+ seenScope = true
+ if !hostspan.isValidIPv6() {
+ return false
+ }
+ hostspan = stringSpan{s.at(i + 1), 0}
+ } else if *s.at(i) == ']' {
+ if seenScope {
+ if !hostspan.isValidScope() {
+ return false
+ }
+ } else if !hostspan.isValidIPv6() {
+ return false
+ }
+ if i == s.len-1 || *s.at((i + 1)) != ':' {
+ return false
+ }
+ return stringSpan{s.at(i + 2), s.len - i - 2}.isValidPort()
+ } else {
+ hostspan.len++
+ }
+ }
+ return false
+ }
+ for i := 0; i < s.len; i++ {
+ if *s.at(i) == ':' {
+ host := stringSpan{s.s, i}
+ port := stringSpan{s.at(i + 1), s.len - i - 1}
+ return port.isValidPort() && (host.isValidIPv4() || host.isValidHostname())
+ }
+ }
+ return false
+}
+
+func (s stringSpan) isValidNetwork() bool {
+ for i := 0; i < s.len; i++ {
+ if *s.at(i) == '/' {
+ ip := stringSpan{s.s, i}
+ cidr := stringSpan{s.at(i + 1), s.len - i - 1}
+ cidrval := uint16(0)
+ if cidr.len > 3 || cidr.len == 0 {
+ return false
+ }
+ for j := 0; j < cidr.len; j++ {
+ if !isDecimal(*cidr.at(j)) {
+ return false
+ }
+ cidrval = 10*cidrval + uint16(*cidr.at(j)-'0')
+ }
+ if ip.isValidIPv4() {
+ return cidrval <= 32
+ } else if ip.isValidIPv6() {
+ return cidrval <= 128
+ }
+ return false
+ }
+ }
+ return s.isValidIPv4() || s.isValidIPv6()
+}
+
+type field int32
+
+const (
+ fieldInterfaceSection field = iota
+ fieldPrivateKey
+ fieldListenPort
+ fieldAddress
+ fieldDNS
+ fieldMTU
+ fieldTable
+ fieldPreUp
+ fieldPostUp
+ fieldPreDown
+ fieldPostDown
+ fieldPeerSection
+ fieldPublicKey
+ fieldPresharedKey
+ fieldAllowedIPs
+ fieldEndpoint
+ fieldPersistentKeepalive
+ fieldInvalid
+)
+
+func sectionForField(t field) field {
+ if t > fieldInterfaceSection && t < fieldPeerSection {
+ return fieldInterfaceSection
+ }
+ if t > fieldPeerSection && t < fieldInvalid {
+ return fieldPeerSection
+ }
+ return fieldInvalid
+}
+
+func (s stringSpan) field() field {
+ switch {
+ case s.isCaselessSame("PrivateKey"):
+ return fieldPrivateKey
+ case s.isCaselessSame("ListenPort"):
+ return fieldListenPort
+ case s.isCaselessSame("Address"):
+ return fieldAddress
+ case s.isCaselessSame("DNS"):
+ return fieldDNS
+ case s.isCaselessSame("MTU"):
+ return fieldMTU
+ case s.isCaselessSame("Table"):
+ return fieldTable
+ case s.isCaselessSame("PublicKey"):
+ return fieldPublicKey
+ case s.isCaselessSame("PresharedKey"):
+ return fieldPresharedKey
+ case s.isCaselessSame("AllowedIPs"):
+ return fieldAllowedIPs
+ case s.isCaselessSame("Endpoint"):
+ return fieldEndpoint
+ case s.isCaselessSame("PersistentKeepalive"):
+ return fieldPersistentKeepalive
+ case s.isCaselessSame("PreUp"):
+ return fieldPreUp
+ case s.isCaselessSame("PostUp"):
+ return fieldPostUp
+ case s.isCaselessSame("PreDown"):
+ return fieldPreDown
+ case s.isCaselessSame("PostDown"):
+ return fieldPostDown
+ }
+ return fieldInvalid
+}
+
+func (s stringSpan) sectionType() field {
+ switch {
+ case s.isCaselessSame("[Peer]"):
+ return fieldPeerSection
+ case s.isCaselessSame("[Interface]"):
+ return fieldInterfaceSection
+ }
+ return fieldInvalid
+}
+
+type highlightSpanArray []highlightSpan
+
+func (hsa *highlightSpanArray) append(o *byte, s stringSpan, t highlight) {
+ if s.len == 0 {
+ return
+ }
+ *hsa = append(*hsa, highlightSpan{t, int((uintptr(unsafe.Pointer(s.s))) - (uintptr(unsafe.Pointer(o)))), s.len})
+}
+
+func (hsa *highlightSpanArray) highlightMultivalueValue(parent, s stringSpan, section field) {
+ switch section {
+ case fieldDNS:
+ if s.isValidIPv4() || s.isValidIPv6() {
+ hsa.append(parent.s, s, highlightIP)
+ } else if s.isValidHostname() {
+ hsa.append(parent.s, s, highlightHost)
+ } else {
+ hsa.append(parent.s, s, highlightError)
+ }
+ case fieldAddress, fieldAllowedIPs:
+ if !s.isValidNetwork() {
+ hsa.append(parent.s, s, highlightError)
+ break
+ }
+ slash := 0
+ for ; slash < s.len; slash++ {
+ if *s.at(slash) == '/' {
+ break
+ }
+ }
+ if slash == s.len {
+ hsa.append(parent.s, s, highlightIP)
+ } else {
+ hsa.append(parent.s, stringSpan{s.s, slash}, highlightIP)
+ hsa.append(parent.s, stringSpan{s.at(slash), 1}, highlightDelimiter)
+ hsa.append(parent.s, stringSpan{s.at(slash + 1), s.len - slash - 1}, highlightCidr)
+ }
+ default:
+ hsa.append(parent.s, s, highlightError)
+ }
+}
+
+func (hsa *highlightSpanArray) highlightMultivalue(parent, s stringSpan, section field) {
+ currentSpan := stringSpan{s.s, 0}
+ lenAtLastSpace := 0
+ for i := 0; i < s.len; i++ {
+ if *s.at(i) == ',' {
+ currentSpan.len = lenAtLastSpace
+ hsa.highlightMultivalueValue(parent, currentSpan, section)
+ hsa.append(parent.s, stringSpan{s.at(i), 1}, highlightDelimiter)
+ lenAtLastSpace = 0
+ currentSpan = stringSpan{s.at(i + 1), 0}
+ } else if *s.at(i) == ' ' || *s.at(i) == '\t' {
+ if s.at(i) == currentSpan.s && currentSpan.len == 0 {
+ currentSpan.s = currentSpan.at(1)
+ } else {
+ currentSpan.len++
+ }
+ } else {
+ currentSpan.len++
+ lenAtLastSpace = currentSpan.len
+ }
+ }
+ currentSpan.len = lenAtLastSpace
+ if currentSpan.len != 0 {
+ hsa.highlightMultivalueValue(parent, currentSpan, section)
+ } else if (*hsa)[len(*hsa)-1].t == highlightDelimiter {
+ (*hsa)[len(*hsa)-1].t = highlightError
+ }
+}
+
+func (hsa *highlightSpanArray) highlightValue(parent, s stringSpan, section field) {
+ switch section {
+ case fieldPrivateKey:
+ hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPrivateKey))
+ case fieldPublicKey:
+ hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPublicKey))
+ case fieldPresharedKey:
+ hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPresharedKey))
+ case fieldMTU:
+ hsa.append(parent.s, s, validateHighlight(s.isValidMTU(), highlightMTU))
+ case fieldTable:
+ hsa.append(parent.s, s, validateHighlight(s.isValidTable(), highlightTable))
+ case fieldPreUp, fieldPostUp, fieldPreDown, fieldPostDown:
+ hsa.append(parent.s, s, validateHighlight(s.isValidPrePostUpDown(), highlightCmd))
+ case fieldListenPort:
+ hsa.append(parent.s, s, validateHighlight(s.isValidPort(), highlightPort))
+ case fieldPersistentKeepalive:
+ hsa.append(parent.s, s, validateHighlight(s.isValidPersistentKeepAlive(), highlightKeepalive))
+ case fieldEndpoint:
+ if !s.isValidEndpoint() {
+ hsa.append(parent.s, s, highlightError)
+ break
+ }
+ colon := s.len
+ for colon > 0 {
+ colon--
+ if *s.at(colon) == ':' {
+ break
+ }
+ }
+ hsa.append(parent.s, stringSpan{s.s, colon}, highlightHost)
+ hsa.append(parent.s, stringSpan{s.at(colon), 1}, highlightDelimiter)
+ hsa.append(parent.s, stringSpan{s.at(colon + 1), s.len - colon - 1}, highlightPort)
+ case fieldAddress, fieldDNS, fieldAllowedIPs:
+ hsa.highlightMultivalue(parent, s, section)
+ default:
+ hsa.append(parent.s, s, highlightError)
+ }
+}
+
+func highlightConfig(config string) []highlightSpan {
+ var ret highlightSpanArray
+ b := append([]byte(config), 0)
+ s := stringSpan{&b[0], len(b) - 1}
+ currentSpan := stringSpan{s.s, 0}
+ currentSection := fieldInvalid
+ currentField := fieldInvalid
+ const (
+ onNone = iota
+ onKey
+ onValue
+ onComment
+ onSection
+ )
+ state := onNone
+ lenAtLastSpace := 0
+ equalsLocation := 0
+ for i := 0; i <= s.len; i++ {
+ if i == s.len || *s.at(i) == '\n' || state != onComment && *s.at(i) == '#' {
+ if state == onKey {
+ currentSpan.len = lenAtLastSpace
+ ret.append(s.s, currentSpan, highlightError)
+ } else if state == onValue {
+ if currentSpan.len != 0 {
+ ret.append(s.s, stringSpan{s.at(equalsLocation), 1}, highlightDelimiter)
+ currentSpan.len = lenAtLastSpace
+ ret.highlightValue(s, currentSpan, currentField)
+ } else {
+ ret.append(s.s, stringSpan{s.at(equalsLocation), 1}, highlightError)
+ }
+ } else if state == onSection {
+ currentSpan.len = lenAtLastSpace
+ currentSection = currentSpan.sectionType()
+ ret.append(s.s, currentSpan, validateHighlight(currentSection != fieldInvalid, highlightSection))
+ } else if state == onComment {
+ ret.append(s.s, currentSpan, highlightComment)
+ }
+ if i == s.len {
+ break
+ }
+ lenAtLastSpace = 0
+ currentField = fieldInvalid
+ if *s.at(i) == '#' {
+ currentSpan = stringSpan{s.at(i), 1}
+ state = onComment
+ } else {
+ currentSpan = stringSpan{s.at(i + 1), 0}
+ state = onNone
+ }
+ } else if state == onComment {
+ currentSpan.len++
+ } else if *s.at(i) == ' ' || *s.at(i) == '\t' {
+ if s.at(i) == currentSpan.s && currentSpan.len == 0 {
+ currentSpan.s = currentSpan.at(1)
+ } else {
+ currentSpan.len++
+ }
+ } else if *s.at(i) == '=' && state == onKey {
+ currentSpan.len = lenAtLastSpace
+ currentField = currentSpan.field()
+ section := sectionForField(currentField)
+ if section == fieldInvalid || currentField == fieldInvalid || section != currentSection {
+ ret.append(s.s, currentSpan, highlightError)
+ } else {
+ ret.append(s.s, currentSpan, highlightField)
+ }
+ equalsLocation = i
+ currentSpan = stringSpan{s.at(i + 1), 0}
+ state = onValue
+ } else {
+ if state == onNone {
+ if *s.at(i) == '[' {
+ state = onSection
+ } else {
+ state = onKey
+ }
+ }
+ currentSpan.len++
+ lenAtLastSpace = currentSpan.len
+ }
+ }
+ return ([]highlightSpan)(ret)
+}
diff --git a/highlighter.h b/highlighter.h
new file mode 100644
index 0000000..30a8484
--- /dev/null
+++ b/highlighter.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2015-2020 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,
+ HighlightTable,
+ HighlightCmd,
+ HighlightError,
+ HighlightEnd
+};
+
+struct highlight_span {
+ enum highlight_type type;
+ size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);