From ebe3f0a0cc389b8c290015f68c9226ab0b9fbbde Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 25 Feb 2019 20:48:42 +0100 Subject: ui: move syntaxedit to sub-package This speeds up compilation time considerably by not needing to invoke cgo for changes in the main UI. Signed-off-by: Jason A. Donenfeld --- ui/syntax/highlighter.c | 620 ++++++++++++++++++++++++++++++++++++++++++++++++ ui/syntax/highlighter.h | 37 +++ ui/syntax/syntaxedit.c | 311 ++++++++++++++++++++++++ ui/syntax/syntaxedit.go | 153 ++++++++++++ ui/syntax/syntaxedit.h | 23 ++ 5 files changed, 1144 insertions(+) create mode 100644 ui/syntax/highlighter.c create mode 100644 ui/syntax/highlighter.h create mode 100644 ui/syntax/syntaxedit.c create mode 100644 ui/syntax/syntaxedit.go create mode 100644 ui/syntax/syntaxedit.h (limited to 'ui/syntax') diff --git a/ui/syntax/highlighter.c b/ui/syntax/highlighter.c new file mode 100644 index 00000000..1913e359 --- /dev/null +++ b/ui/syntax/highlighter.c @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved. + */ + +#include +#include +#include +#include +#include +#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 ((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); +} + +#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/ui/syntax/highlighter.h b/ui/syntax/highlighter.h new file mode 100644 index 00000000..c004e127 --- /dev/null +++ b/ui/syntax/highlighter.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved. + */ + +#include + +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); diff --git a/ui/syntax/syntaxedit.c b/ui/syntax/syntaxedit.c new file mode 100644 index 00000000..5586e9d1 --- /dev/null +++ b/ui/syntax/syntaxedit.c @@ -0,0 +1,311 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "syntaxedit.h" +#include "highlighter.h" + +const GUID CDECL IID_ITextDocument = { 0x8CC497C0, 0xA1DF, 0x11CE, { 0x80, 0x98, 0x00, 0xAA, 0x00, 0x47, 0xBE, 0x5D } }; + +struct syntaxedit_data { + IRichEditOle *irich; + ITextDocument *idoc; +}; + +static WNDPROC parent_proc; + +struct span_style { + COLORREF color; + DWORD effects; +}; + +static const struct span_style stylemap[] = { + [HighlightSection] = { .color = RGB(0x32, 0x6D, 0x74), .effects = CFE_BOLD }, + [HighlightField] = { .color = RGB(0x9B, 0x23, 0x93), .effects = CFE_BOLD }, + [HighlightPrivateKey] = { .color = RGB(0x64, 0x38, 0x20) }, + [HighlightPublicKey] = { .color = RGB(0x64, 0x38, 0x20) }, + [HighlightPresharedKey] = { .color = RGB(0x64, 0x38, 0x20) }, + [HighlightIP] = { .color = RGB(0x0E, 0x0E, 0xFF) }, + [HighlightCidr] = { .color = RGB(0x81, 0x5F, 0x03) }, + [HighlightHost] = { .color = RGB(0x0E, 0x0E, 0xFF) }, + [HighlightPort] = { .color = RGB(0x81, 0x5F, 0x03) }, + [HighlightMTU] = { .color = RGB(0x1C, 0x00, 0xCF) }, + [HighlightKeepalive] = { .color = RGB(0x1C, 0x00, 0xCF) }, + [HighlightComment] = { .color = RGB(0x53, 0x65, 0x79), .effects = CFE_ITALIC }, + [HighlightDelimiter] = { .color = RGB(0x00, 0x00, 0x00) }, +#ifndef MOBILE_WGQUICK_SUBSET + [HighlightTable] = { .color = RGB(0x1C, 0x00, 0xCF) }, + [HighlightFwMark] = { .color = RGB(0x1C, 0x00, 0xCF) }, + [HighlightSaveConfig] = { .color = RGB(0x81, 0x5F, 0x03) }, + [HighlightCmd] = { .color = RGB(0x63, 0x75, 0x89) }, +#endif + [HighlightError] = { .color = RGB(0xC4, 0x1A, 0x16), .effects = CFE_UNDERLINE } +}; + +static void highlight_text(HWND hWnd) +{ + GETTEXTLENGTHEX gettextlengthex = { + .flags = GTL_NUMBYTES, + .codepage = CP_ACP /* Probably CP_UTF8 would be better, but (wine at least) returns utf32 sizes. */ + }; + GETTEXTEX gettextex = { + .flags = GT_NOHIDDENTEXT, + .codepage = gettextlengthex.codepage + }; + CHARFORMAT2 format = { + .cbSize = sizeof(CHARFORMAT2), + .dwMask = CFM_COLOR | CFM_CHARSET | CFM_SIZE | CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE, + .dwEffects = CFE_AUTOCOLOR, + .yHeight = 20 * 10, + .bCharSet = ANSI_CHARSET + }; + struct syntaxedit_data *this = (struct syntaxedit_data *)GetWindowLongPtr(hWnd, GWLP_USERDATA); + LRESULT msg_size; + char *msg; + struct highlight_span *spans; + CHARRANGE orig_selection; + POINT original_scroll; + bool found_private_key = false; + + msg_size = SendMessage(hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gettextlengthex, 0); + if (msg_size == E_INVALIDARG) + return; + gettextex.cb = msg_size + 1; + + msg = malloc(msg_size + 1); + if (!msg) + return; + if (SendMessage(hWnd, EM_GETTEXTEX, (WPARAM)&gettextex, (LPARAM)msg) <= 0) { + free(msg); + return; + } + + /* By default we get CR not CRLF, so just convert to LF. */ + for (size_t i = 0; i < msg_size; ++i) { + if (msg[i] == '\r') + msg[i] = '\n'; + } + + spans = highlight_config(msg); + if (!spans) { + free(msg); + return; + } + + this->idoc->lpVtbl->Undo(this->idoc, tomSuspend, NULL); + SendMessage(hWnd, WM_SETREDRAW, FALSE, 0); + SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&orig_selection); + SendMessage(hWnd, EM_GETSCROLLPOS, 0, (LPARAM)&original_scroll); + SendMessage(hWnd, EM_HIDESELECTION, TRUE, 0); + SendMessage(hWnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format); + for (struct highlight_span *span = spans; span->type != HighlightEnd; ++span) { + CHARRANGE selection = { span->start, span->len + span->start }; + SendMessage(hWnd, EM_EXSETSEL, 0, (LPARAM)&selection); + format.crTextColor = stylemap[span->type].color; + format.dwEffects = stylemap[span->type].effects; + SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&format); + if (span->type == HighlightPrivateKey && !found_private_key) { + /* Rather than allocating a new string, we mangle this one, since (for now) we don't use msg again. */ + msg[span->start + span->len] = '\0'; + SendMessage(hWnd, SE_PRIVATE_KEY, 0, (LPARAM)&msg[span->start]); + found_private_key = true; + } + } + SendMessage(hWnd, EM_SETSCROLLPOS, 0, (LPARAM)&original_scroll); + SendMessage(hWnd, EM_EXSETSEL, 0, (LPARAM)&orig_selection); + SendMessage(hWnd, EM_HIDESELECTION, FALSE, 0); + SendMessage(hWnd, WM_SETREDRAW, TRUE, 0); + RedrawWindow(hWnd, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); + this->idoc->lpVtbl->Undo(this->idoc, tomResume, NULL); + free(spans); + free(msg); + if (!found_private_key) + SendMessage(hWnd, SE_PRIVATE_KEY, 0, 0); +} + +static void context_menu(HWND hWnd, INT x, INT y) +{ + GETTEXTLENGTHEX gettextlengthex = { + .flags = GTL_DEFAULT, + .codepage = CP_ACP + }; + /* This disturbing hack grabs the system edit menu normally used for the EDIT control. */ + HMENU popup, menu = LoadMenuW(GetModuleHandleW(L"comctl32.dll"), MAKEINTRESOURCEW(1)); + CHARRANGE selection = { 0 }; + bool has_selection, can_selectall, can_undo, can_paste; + UINT cmd; + + if (!menu) + return; + + SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&selection); + has_selection = selection.cpMax - selection.cpMin; + can_selectall = selection.cpMin || (selection.cpMax < SendMessage(hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gettextlengthex, 0)); + can_undo = SendMessage(hWnd, EM_CANUNDO, 0, 0); + can_paste = SendMessage(hWnd, EM_CANPASTE, CF_TEXT, 0); + + popup = GetSubMenu(menu, 0); + EnableMenuItem(popup, WM_UNDO, MF_BYCOMMAND | (can_undo ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, WM_CUT, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, WM_COPY, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, WM_PASTE, MF_BYCOMMAND | (can_paste ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, WM_CLEAR, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, EM_SETSEL, MF_BYCOMMAND | (can_selectall ? MF_ENABLED : MF_GRAYED)); + + /* Delete items that we don't handle. */ + for (int ctl = GetMenuItemCount(popup) - 1; ctl >= 0; --ctl) { + MENUITEMINFOW menu_item = { + .cbSize = sizeof(MENUITEMINFOW), + .fMask = MIIM_FTYPE | MIIM_ID + }; + if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item)) + continue; + if (menu_item.fType & MFT_SEPARATOR) + continue; + switch (menu_item.wID) { + case WM_UNDO: + case WM_CUT: + case WM_COPY: + case WM_PASTE: + case WM_CLEAR: + case EM_SETSEL: + continue; + } + DeleteMenu(popup, ctl, MF_BYPOSITION); + } + /* Delete trailing and adjacent separators. */ + for (int ctl = GetMenuItemCount(popup) - 1, end = true; ctl >= 0; --ctl) { + MENUITEMINFOW menu_item = { + .cbSize = sizeof(MENUITEMINFOW), + .fMask = MIIM_FTYPE + }; + if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item)) + continue; + if (!(menu_item.fType & MFT_SEPARATOR)) { + end = false; + continue; + } + if (!end && ctl) { + if (!GetMenuItemInfoW(popup, ctl - 1, MF_BYPOSITION, &menu_item)) + continue; + if (!(menu_item.fType & MFT_SEPARATOR)) + continue; + } + DeleteMenu(popup, ctl, MF_BYPOSITION); + } + + if (x == -1 && y == -1) { + RECT rect; + GetWindowRect(hWnd, &rect); + x = rect.left + (rect.right - rect.left) / 2; + y = rect.top + (rect.bottom - rect.top) / 2; + } + + if (GetFocus() != hWnd) + SetFocus(hWnd); + + cmd = TrackPopupMenu(popup, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, x, y, 0, hWnd, NULL); + if (cmd) + SendMessage(hWnd, cmd, 0, cmd == EM_SETSEL ? -1 : 0); + + DestroyMenu(menu); +} + +static LRESULT CALLBACK child_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + switch (Msg) { + case WM_CREATE: { + struct syntaxedit_data *this = calloc(1, sizeof(*this)); + assert(this); + SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this); + SendMessage(hWnd, EM_GETOLEINTERFACE, 0, (LPARAM)&this->irich); + assert(this->irich); + this->irich->lpVtbl->QueryInterface(this->irich, &IID_ITextDocument, (void **)&this->idoc); + assert(this->idoc); + SendMessage(hWnd, EM_SETEVENTMASK, 0, ENM_CHANGE); + SendMessage(hWnd, EM_SETTEXTMODE, TM_SINGLECODEPAGE, 0); + break; + } + case WM_DESTROY: { + struct syntaxedit_data *this = (struct syntaxedit_data *)GetWindowLongPtr(hWnd, GWLP_USERDATA); + this->idoc->lpVtbl->Release(this->idoc); + this->irich->lpVtbl->Release(this->irich); + free(this); + } + case WM_SETTEXT: { + LRESULT ret = parent_proc(hWnd, Msg, wParam, lParam); + highlight_text(hWnd); + SendMessage(hWnd, EM_EMPTYUNDOBUFFER, 0, 0); + return ret; + } + case WM_REFLECT + WM_COMMAND: + case WM_COMMAND: + case WM_REFLECT + WM_NOTIFY: + case WM_NOTIFY: + switch (HIWORD(wParam)) { + case EN_CHANGE: + highlight_text(hWnd); + break; + } + break; + case WM_PASTE: + SendMessage(hWnd, EM_PASTESPECIAL, CF_TEXT, 0); + return 0; + case WM_KEYDOWN: + if (!(GetKeyState(VK_CONTROL) & 0x8000)) + break; + switch (LOWORD(wParam)) { + case 'V': + SendMessage(hWnd, EM_PASTESPECIAL, CF_TEXT, 0); + return 0; + } + break; + case WM_CONTEXTMENU: + context_menu(hWnd, LOWORD(lParam), HIWORD(lParam)); + return 0; + } + return parent_proc(hWnd, Msg, wParam, lParam); +} + +bool register_syntax_edit(void) +{ + WNDCLASSEXW class = { .cbSize = sizeof(WNDCLASSEXW) }; + WNDPROC pp; + HANDLE lib; + + if (parent_proc) + return true; + + lib = LoadLibraryW(L"msftedit.dll"); + if (!lib) + return false; + + if (!GetClassInfoExW(NULL, L"RICHEDIT50W", &class)) + goto err; + pp = class.lpfnWndProc; + if (!pp) + goto err; + class.cbSize = sizeof(WNDCLASSEXW); + class.hInstance = GetModuleHandleW(NULL); + class.lpszClassName = L"WgQuickSyntaxEdit"; + class.lpfnWndProc = child_proc; + if (!RegisterClassExW(&class)) + goto err; + parent_proc = pp; + return true; + +err: + FreeLibrary(lib); + return false; +} diff --git a/ui/syntax/syntaxedit.go b/ui/syntax/syntaxedit.go new file mode 100644 index 00000000..547c65ca --- /dev/null +++ b/ui/syntax/syntaxedit.go @@ -0,0 +1,153 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package syntax + +import ( + "errors" + "strings" + "syscall" + "unsafe" + + "golang.zx2c4.com/wireguard/windows/ui/internal/walk" + "golang.zx2c4.com/wireguard/windows/ui/internal/walk/win" +) + +// #include "syntaxedit.h" +import "C" + +type PrivateKeyHandler func(privateKey string) +type PrivateKeyEvent struct { + handlers []PrivateKeyHandler +} + +func (e *PrivateKeyEvent) Attach(handler PrivateKeyHandler) int { + for i, h := range e.handlers { + if h == nil { + e.handlers[i] = handler + return i + } + } + + e.handlers = append(e.handlers, handler) + return len(e.handlers) - 1 +} +func (e *PrivateKeyEvent) Detach(handle int) { + e.handlers[handle] = nil +} + +type PrivateKeyPublisher struct { + event PrivateKeyEvent +} + +func (p *PrivateKeyPublisher) Event() *PrivateKeyEvent { + return &p.event +} +func (p *PrivateKeyPublisher) Publish(privateKey string) { + for _, handler := range p.event.handlers { + if handler != nil { + handler(privateKey) + } + } +} + +type SyntaxEdit struct { + walk.WidgetBase + textChangedPublisher walk.EventPublisher + privateKeyPublisher PrivateKeyPublisher +} + +func init() { + C.register_syntax_edit() +} + +func (se *SyntaxEdit) LayoutFlags() walk.LayoutFlags { + return walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert +} + +func (se *SyntaxEdit) MinSizeHint() walk.Size { + return walk.Size{20, 12} +} + +func (se *SyntaxEdit) SizeHint() walk.Size { + return walk.Size{200, 100} +} + +func (se *SyntaxEdit) Text() string { + textLength := se.SendMessage(win.WM_GETTEXTLENGTH, 0, 0) + buf := make([]uint16, textLength+1) + se.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0]))) + return strings.Replace(syscall.UTF16ToString(buf), "\r\n", "\n", -1) +} + +func (se *SyntaxEdit) SetText(text string) (err error) { + if text == se.Text() { + return nil + } + text = strings.Replace(text, "\n", "\r\n", -1) + if win.TRUE != se.SendMessage(win.WM_SETTEXT, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text)))) { + err = errors.New("WM_SETTEXT failed") + } + se.textChangedPublisher.Publish() + return +} + +func (se *SyntaxEdit) TextChanged() *walk.Event { + return se.textChangedPublisher.Event() +} + +func (se *SyntaxEdit) PrivateKeyChanged() *PrivateKeyEvent { + return se.privateKeyPublisher.Event() +} + +func (se *SyntaxEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { + switch msg { + case win.WM_NOTIFY, win.WM_COMMAND: + switch win.HIWORD(uint32(wParam)) { + case win.EN_CHANGE: + se.textChangedPublisher.Publish() + } + // This is a horrible trick from MFC where we reflect the event back to the child. + se.SendMessage(msg+C.WM_REFLECT, wParam, lParam) + case C.SE_PRIVATE_KEY: + if lParam == 0 { + se.privateKeyPublisher.Publish("") + } else { + se.privateKeyPublisher.Publish(C.GoString((*C.char)(unsafe.Pointer(lParam)))) + } + } + return se.WidgetBase.WndProc(hwnd, msg, wParam, lParam) +} + +func NewSyntaxEdit(parent walk.Container) (*SyntaxEdit, error) { + se := &SyntaxEdit{} + err := walk.InitWidget( + se, + parent, + "WgQuickSyntaxEdit", + C.SYNTAXEDIT_STYLE, + C.SYNTAXEDIT_EXTSTYLE, + ) + if err != nil { + return nil, err + } + + se.GraphicsEffects().Add(walk.InteractionEffect) + se.GraphicsEffects().Add(walk.FocusEffect) + se.MustRegisterProperty("Text", walk.NewProperty( + func() interface{} { + return se.Text() + }, + func(v interface{}) error { + if s, ok := v.(string); ok { + return se.SetText(s) + } else { + return se.SetText("") + } + }, + se.textChangedPublisher.Event())) + + return se, nil +} diff --git a/ui/syntax/syntaxedit.h b/ui/syntax/syntaxedit.h new file mode 100644 index 00000000..12815278 --- /dev/null +++ b/ui/syntax/syntaxedit.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#ifndef SYNTAXEDIT_H +#define SYNTAXEDIT_H + +#include +#include +#include + +#define SYNTAXEDIT_STYLE (WS_CHILD | WS_CLIPSIBLINGS | ES_MULTILINE | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_BORDER | WS_TABSTOP | ES_WANTRETURN | ES_NOOLEDRAGDROP) +#define SYNTAXEDIT_EXTSTYLE (WS_EX_CLIENTEDGE) + +/* The old MFC reflection trick. */ +#define WM_REFLECT (WM_USER + 0x1C00) + +#define SE_PRIVATE_KEY (WM_USER + 0x3100) + +extern bool register_syntax_edit(void); + +#endif -- cgit v1.2.3-59-g8ed1b