diff --git a/dlangide-monod-linux.dproj b/dlangide-monod-linux.dproj
index 1b6ebdf..717f216 100644
--- a/dlangide-monod-linux.dproj
+++ b/dlangide-monod-linux.dproj
@@ -224,6 +224,7 @@
+
diff --git a/dlangide-monod-osx.dproj b/dlangide-monod-osx.dproj
index 89f0ac5..a4e84a5 100644
--- a/dlangide-monod-osx.dproj
+++ b/dlangide-monod-osx.dproj
@@ -135,6 +135,7 @@
+
diff --git a/dlangide_msvc.visualdproj b/dlangide_msvc.visualdproj
index 089eb3a..79312ec 100644
--- a/dlangide_msvc.visualdproj
+++ b/dlangide_msvc.visualdproj
@@ -427,6 +427,7 @@
+
diff --git a/src/ddebug/gdb/gdbinterface.d b/src/ddebug/gdb/gdbinterface.d
index 079e0e4..593ece2 100644
--- a/src/ddebug/gdb/gdbinterface.d
+++ b/src/ddebug/gdb/gdbinterface.d
@@ -5,6 +5,7 @@ import ddebug.common.execution;
import dlangui.core.logger;
import ddebug.common.queue;
import dlangide.builders.extprocess;
+import ddebug.gdb.gdbmiparser;
import std.utf;
import std.conv : to;
import std.array : empty;
@@ -420,6 +421,7 @@ class GDBInterface : ConsoleDebuggerInterface {
ResultClass msgId = resultByName(msgType);
if (msgId == ResultClass.other)
Log.d("GDB WARN unknown result class type: ", msgType);
+ MIValue params = parseMI(s);
Log.v("GDB result ^[", token, "] ", msgType, " params: ", s);
}
@@ -482,437 +484,3 @@ class GDBInterface : ConsoleDebuggerInterface {
}
-string parseIdent(ref string s) {
- string res = null;
- int len = 0;
- for(; len < s.length; len++) {
- char ch = s[len];
- if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-'))
- break;
- }
- if (len > 0) {
- res = s[0..len];
- s = s[len .. $];
- }
- return res;
-}
-
-bool skipComma(ref string s) {
- if (s.length > 0 && s[0] == ',') {
- s = s[1 .. $];
- return true;
- }
- return false;
-}
-
-string parseIdentAndSkipComma(ref string s) {
- string res = parseIdent(s);
- skipComma(s);
- return res;
-}
-
-ResultClass resultByName(string s) {
- if (s.equal("done")) return ResultClass.done;
- if (s.equal("running")) return ResultClass.running;
- if (s.equal("connected")) return ResultClass.connected;
- if (s.equal("error")) return ResultClass.error;
- if (s.equal("exit")) return ResultClass.exit;
- return ResultClass.other;
-}
-
-enum ResultClass {
- done,
- running,
- connected,
- error,
- exit,
- other
-}
-
-AsyncClass asyncByName(string s) {
- if (s.equal("stopped")) return AsyncClass.stopped;
- if (s.equal("running")) return AsyncClass.running;
- if (s.equal("library-loaded")) return AsyncClass.library_loaded;
- if (s.equal("library-unloaded")) return AsyncClass.library_unloaded;
- if (s.equal("thread-group-added")) return AsyncClass.thread_group_added;
- if (s.equal("thread-group-started")) return AsyncClass.thread_group_started;
- if (s.equal("thread-group-exited")) return AsyncClass.thread_group_exited;
- if (s.equal("thread-created")) return AsyncClass.thread_created;
- if (s.equal("thread-exited")) return AsyncClass.thread_exited;
- return AsyncClass.other;
-}
-
-enum AsyncClass {
- running,
- stopped,
- library_loaded,
- library_unloaded,
- thread_group_added,
- thread_group_started,
- thread_group_exited,
- thread_created,
- thread_exited,
- other
-}
-
-enum MITokenType {
- /// end of line
- eol,
- /// error
- error,
- /// identifier
- ident,
- /// C string
- str,
- /// = sign
- eq,
- /// , sign
- comma,
- /// { brace
- curlyOpen,
- /// } brace
- curlyClose,
- /// [ brace
- squareOpen,
- /// ] brace
- squareClose,
-}
-
-struct MIToken {
- MITokenType type;
- string str;
- this(MITokenType type, string str = null) {
- this.type = type;
- this.str = str;
- }
-}
-
-MIToken parseMIToken(ref string s) {
- if (s.empty)
- return MIToken(MITokenType.eol);
- char ch = s[0];
- if (ch == ',') {
- s = s[1..$];
- return MIToken(MITokenType.comma, ",");
- }
- if (ch == '=') {
- s = s[1..$];
- return MIToken(MITokenType.eq, "=");
- }
- if (ch == '{') {
- s = s[1..$];
- return MIToken(MITokenType.curlyOpen, "{");
- }
- if (ch == '}') {
- s = s[1..$];
- return MIToken(MITokenType.curlyClose, "}");
- }
- if (ch == '[') {
- s = s[1..$];
- return MIToken(MITokenType.squareOpen, "[");
- }
- if (ch == ']') {
- s = s[1..$];
- return MIToken(MITokenType.squareClose, "]");
- }
- // C string
- if (ch == '\"') {
- string str = parseCString(s);
- if (!str.ptr) {
- return MIToken(MITokenType.error);
- }
- return MIToken(MITokenType.str, str);
- }
- // identifier
- string str = parseIdent(s);
- if (!str.empty)
- return MIToken(MITokenType.ident, str);
- return MIToken(MITokenType.error);
-}
-
-/// tokenize GDB MI output into array of tokens
-MIToken[] tokenizeMI(string s, out bool error) {
- error = false;
- string src = s;
- MIToken[] res;
- for(;;) {
- MIToken token = parseMIToken(s);
- if (token.type == MITokenType.eol)
- break;
- if (token.type == MITokenType.error) {
- error = true;
- Log.e("Error while tokenizing GDB output ", src, " near ", s);
- break;
- }
- res ~= token;
- }
- return res;
-}
-
-MIValue parseMI(string s) {
- string src = s;
- try {
- bool err = false;
- MIToken[] tokens = tokenizeMI(s, err);
- if (err) {
- // tokenizer error
- return null;
- }
- MIValue[] items = parseMIList(tokens);
- return new MIList(items);
- } catch (Exception e) {
- Log.e("Cannot parse MI from " ~ src, e);
- return null;
- }
-}
-
-MIValue parseMIValue(ref MIToken[] tokens) {
- if (tokens.length == 0)
- return null;
- MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
- MITokenType nextTokenType = tokens.length > 1 ? tokens[1].type : MITokenType.eol;
- if (tokenType == MITokenType.ident) {
- string ident = tokens[0].str;
- if (nextTokenType == MITokenType.eol || nextTokenType == MITokenType.comma) {
- MIValue res = new MIIdent(ident);
- tokens = tokens[1..$];
- return res;
- } else if (nextTokenType == MITokenType.eq) {
- tokens = tokens[1..$];
- MIValue value = parseMIValue(tokens);
- MIValue res = new MIKeyValue(ident, value);
- return res;
- }
- throw new Exception("Unexpected token " ~ to!string(tokenType));
- } else if (tokenType == MITokenType.str) {
- string str = tokens[0].str;
- tokens = tokens[1..$];
- MIValue res = new MIString(str);
- } else if (tokenType == MITokenType.curlyOpen) {
- tokens = tokens[1..$];
- MIValue[] list = parseMIList(tokens, MITokenType.curlyClose);
- return new MIMap(list);
- } else if (tokenType == MITokenType.squareOpen) {
- tokens = tokens[1..$];
- MIValue[] list = parseMIList(tokens, MITokenType.squareClose);
- return new MIList(list);
- }
- throw new Exception("Invalid token at end of list: " ~ tokenType.to!string);
-}
-
-MIValue[] parseMIList(ref MIToken[] tokens, MITokenType closingToken = MITokenType.eol) {
- MIValue[] res;
- for (;;) {
- MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
- if (tokenType == MITokenType.eol)
- return res;
- if (tokenType == closingToken) {
- tokens = tokens[1..$];
- return res;
- }
- MIValue value = parseMIValue(tokens);
- res ~= value;
- tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
- if (tokenType == MITokenType.comma) {
- tokens = tokens[1..$];
- continue;
- }
- throw new Exception("Unexpected token in list " ~ to!string(tokenType));
- }
-}
-
-enum MIValueType {
- /// ident
- empty,
- /// ident
- ident,
- /// c-string
- str,
- /// key=value pair
- keyValue,
- /// list []
- list,
- /// map {key=value, ...}
- map,
-}
-
-class MIValue {
- MIValueType type;
- this(MIValueType type) {
- this.type = type;
- }
- @property string str() { return null; }
- @property int length() { return 1; }
- MIValue opIndex(int index) { return null; }
- MIValue opIndex(string key) { return null; }
-}
-
-class MIKeyValue : MIValue {
- private string _key;
- private MIValue _value;
- this(string key, MIValue value) {
- super(MIValueType.keyValue);
- _key = key;
- _value = value;
- }
- @property string key() { return _key; }
- @property string str() { return _key; }
- @property MIValue value() { return _value; }
-}
-
-class MIIdent : MIValue {
- private string _ident;
- this(string ident) {
- super(MIValueType.ident);
- _ident = ident;
- }
- override @property string str() { return _ident; }
-}
-
-class MIString : MIValue {
- private string _str;
- this(string str) {
- super(MIValueType.str);
- _str = str;
- }
- override @property string str() { return _str; }
-}
-
-class MIList : MIValue {
- private MIValue[] _items;
- private MIValue[string] _map;
-
- override @property int length() { return cast(int)_items.length; }
- override MIValue opIndex(int index) {
- if (index < 0 || index >= _items.length)
- return null;
- return _items[index];
- }
-
- override MIValue opIndex(string key) {
- if (key in _map) {
- MIValue res = _map[key];
- return res;
- }
- return null;
- }
-
- this(MIValue[] items) {
- super(MIValueType.list);
- _items = items;
- // fill map
- foreach(item; _items) {
- if (item.type == MIValueType.keyValue) {
- if (!item.str.empty)
- _map[item.str] = (cast(MIKeyValue)item).value;
- }
- }
- }
-}
-
-class MIMap : MIList {
- this(MIValue[] items) {
- super(items);
- type = MIValueType.map;
- }
-}
-
-private char nextChar(ref string s) {
- if (s.empty)
- return 0;
- char ch = s[0];
- s = s[1 .. $];
- return ch;
-}
-
-string parseCString(ref string s) {
- char[] res;
- // skip opening "
- char ch = nextChar(s);
- if (!ch)
- return null;
- s = s[1 .. $];
- if (ch != '\"')
- return null;
- for (;;) {
- if (s.empty) {
- // unexpected end of string
- return null;
- }
- ch = nextChar(s);
- if (ch == '\"')
- break;
- if (ch == '\\') {
- // escape sequence
- ch = nextChar(s);
- if (ch >= '0' && ch <= '7') {
- // octal
- int number = (ch - '0');
- char ch2 = nextChar(s);
- char ch3 = nextChar(s);
- if (ch2 < '0' || ch2 > '7')
- return null;
- if (ch3 < '0' || ch3 > '7')
- return null;
- number = number * 8 + (ch2 - '0');
- number = number * 8 + (ch3 - '0');
- if (number > 255)
- return null; // invalid octal number
- res ~= cast(char)number;
- } else {
- switch (ch) {
- case 'n':
- res ~= '\n';
- break;
- case 'r':
- res ~= '\r';
- break;
- case 't':
- res ~= '\t';
- break;
- case 'a':
- res ~= '\a';
- break;
- case 'b':
- res ~= '\b';
- break;
- case 'f':
- res ~= '\f';
- break;
- case 'v':
- res ~= '\v';
- break;
- case 'x': {
- // 2 digit hex number
- uint ch2 = decodeHexDigit(nextChar(s));
- uint ch3 = decodeHexDigit(nextChar(s));
- if (ch2 > 15 || ch3 > 15)
- return null;
- res ~= cast(char)((ch2 << 4) | ch3);
- break;
- }
- default:
- res ~= ch;
- break;
- }
- }
- } else {
- res ~= ch;
- }
- }
- if (!res.length)
- return "";
- return res.dup;
-}
-
-/// decodes hex digit (0..9, a..f, A..F), returns uint.max if invalid
-private uint decodeHexDigit(T)(T ch) {
- if (ch >= '0' && ch <= '9')
- return ch - '0';
- else if (ch >= 'a' && ch <= 'f')
- return ch - 'a' + 10;
- else if (ch >= 'A' && ch <= 'F')
- return ch - 'A' + 10;
- return uint.max;
-}
-
diff --git a/src/ddebug/gdb/gdbmiparser.d b/src/ddebug/gdb/gdbmiparser.d
new file mode 100644
index 0000000..d2b895c
--- /dev/null
+++ b/src/ddebug/gdb/gdbmiparser.d
@@ -0,0 +1,445 @@
+module ddebug.gdb.gdbmiparser;
+
+import dlangui.core.logger;
+import std.utf;
+import std.conv : to;
+import std.array : empty;
+import std.algorithm : startsWith, equal;
+
+string parseIdent(ref string s) {
+ string res = null;
+ int len = 0;
+ for(; len < s.length; len++) {
+ char ch = s[len];
+ if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-'))
+ break;
+ }
+ if (len > 0) {
+ res = s[0..len];
+ s = s[len .. $];
+ }
+ return res;
+}
+
+bool skipComma(ref string s) {
+ if (s.length > 0 && s[0] == ',') {
+ s = s[1 .. $];
+ return true;
+ }
+ return false;
+}
+
+string parseIdentAndSkipComma(ref string s) {
+ string res = parseIdent(s);
+ skipComma(s);
+ return res;
+}
+
+ResultClass resultByName(string s) {
+ if (s.equal("done")) return ResultClass.done;
+ if (s.equal("running")) return ResultClass.running;
+ if (s.equal("connected")) return ResultClass.connected;
+ if (s.equal("error")) return ResultClass.error;
+ if (s.equal("exit")) return ResultClass.exit;
+ return ResultClass.other;
+}
+
+enum ResultClass {
+ done,
+ running,
+ connected,
+ error,
+ exit,
+ other
+}
+
+AsyncClass asyncByName(string s) {
+ if (s.equal("stopped")) return AsyncClass.stopped;
+ if (s.equal("running")) return AsyncClass.running;
+ if (s.equal("library-loaded")) return AsyncClass.library_loaded;
+ if (s.equal("library-unloaded")) return AsyncClass.library_unloaded;
+ if (s.equal("thread-group-added")) return AsyncClass.thread_group_added;
+ if (s.equal("thread-group-started")) return AsyncClass.thread_group_started;
+ if (s.equal("thread-group-exited")) return AsyncClass.thread_group_exited;
+ if (s.equal("thread-created")) return AsyncClass.thread_created;
+ if (s.equal("thread-exited")) return AsyncClass.thread_exited;
+ return AsyncClass.other;
+}
+
+enum AsyncClass {
+ running,
+ stopped,
+ library_loaded,
+ library_unloaded,
+ thread_group_added,
+ thread_group_started,
+ thread_group_exited,
+ thread_created,
+ thread_exited,
+ other
+}
+
+enum MITokenType {
+ /// end of line
+ eol,
+ /// error
+ error,
+ /// identifier
+ ident,
+ /// C string
+ str,
+ /// = sign
+ eq,
+ /// , sign
+ comma,
+ /// { brace
+ curlyOpen,
+ /// } brace
+ curlyClose,
+ /// [ brace
+ squareOpen,
+ /// ] brace
+ squareClose,
+}
+
+struct MIToken {
+ MITokenType type;
+ string str;
+ this(MITokenType type, string str = null) {
+ this.type = type;
+ this.str = str;
+ }
+}
+
+MIToken parseMIToken(ref string s) {
+ if (s.empty)
+ return MIToken(MITokenType.eol);
+ char ch = s[0];
+ if (ch == ',') {
+ s = s[1..$];
+ return MIToken(MITokenType.comma, ",");
+ }
+ if (ch == '=') {
+ s = s[1..$];
+ return MIToken(MITokenType.eq, "=");
+ }
+ if (ch == '{') {
+ s = s[1..$];
+ return MIToken(MITokenType.curlyOpen, "{");
+ }
+ if (ch == '}') {
+ s = s[1..$];
+ return MIToken(MITokenType.curlyClose, "}");
+ }
+ if (ch == '[') {
+ s = s[1..$];
+ return MIToken(MITokenType.squareOpen, "[");
+ }
+ if (ch == ']') {
+ s = s[1..$];
+ return MIToken(MITokenType.squareClose, "]");
+ }
+ // C string
+ if (ch == '\"') {
+ string str = parseCString(s);
+ if (!str.ptr) {
+ return MIToken(MITokenType.error);
+ }
+ return MIToken(MITokenType.str, str);
+ }
+ // identifier
+ string str = parseIdent(s);
+ if (!str.empty)
+ return MIToken(MITokenType.ident, str);
+ return MIToken(MITokenType.error);
+}
+
+/// tokenize GDB MI output into array of tokens
+MIToken[] tokenizeMI(string s, out bool error) {
+ error = false;
+ string src = s;
+ MIToken[] res;
+ for(;;) {
+ MIToken token = parseMIToken(s);
+ if (token.type == MITokenType.eol)
+ break;
+ if (token.type == MITokenType.error) {
+ error = true;
+ Log.e("Error while tokenizing GDB output ", src, " near ", s);
+ break;
+ }
+ res ~= token;
+ }
+ return res;
+}
+
+/// Parse GDB MI output string
+MIValue parseMI(string s) {
+ string src = s;
+ try {
+ bool err = false;
+ MIToken[] tokens = tokenizeMI(s, err);
+ if (err) {
+ // tokenizer error
+ return null;
+ }
+ MIValue[] items = parseMIList(tokens);
+ return new MIList(items);
+ } catch (Exception e) {
+ Log.e("Cannot parse MI from " ~ src, e);
+ return null;
+ }
+}
+
+MIValue parseMIValue(ref MIToken[] tokens) {
+ if (tokens.length == 0)
+ return null;
+ MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
+ MITokenType nextTokenType = tokens.length > 1 ? tokens[1].type : MITokenType.eol;
+ if (tokenType == MITokenType.ident) {
+ string ident = tokens[0].str;
+ if (nextTokenType == MITokenType.eol || nextTokenType == MITokenType.comma) {
+ MIValue res = new MIIdent(ident);
+ tokens = tokens[1..$];
+ return res;
+ } else if (nextTokenType == MITokenType.eq) {
+ tokens = tokens[1..$]; // skip ident
+ tokens = tokens[1..$]; // skip =
+ MIValue value = parseMIValue(tokens);
+ tokens = tokens[1..$]; // skip value
+ MIValue res = new MIKeyValue(ident, value);
+ return res;
+ }
+ throw new Exception("Unexpected token " ~ to!string(tokenType));
+ } else if (tokenType == MITokenType.str) {
+ string str = tokens[0].str;
+ tokens = tokens[1..$];
+ MIValue res = new MIString(str);
+ } else if (tokenType == MITokenType.curlyOpen) {
+ tokens = tokens[1..$];
+ MIValue[] list = parseMIList(tokens, MITokenType.curlyClose);
+ return new MIMap(list);
+ } else if (tokenType == MITokenType.squareOpen) {
+ tokens = tokens[1..$];
+ MIValue[] list = parseMIList(tokens, MITokenType.squareClose);
+ return new MIList(list);
+ }
+ throw new Exception("Invalid token at end of list: " ~ tokenType.to!string);
+}
+
+MIValue[] parseMIList(ref MIToken[] tokens, MITokenType closingToken = MITokenType.eol) {
+ MIValue[] res;
+ for (;;) {
+ MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
+ if (tokenType == MITokenType.eol)
+ return res;
+ if (tokenType == closingToken) {
+ tokens = tokens[1..$];
+ return res;
+ }
+ MIValue value = parseMIValue(tokens);
+ res ~= value;
+ tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
+ if (tokenType == MITokenType.comma) {
+ tokens = tokens[1..$];
+ continue;
+ }
+ throw new Exception("Unexpected token in list " ~ to!string(tokenType));
+ }
+}
+
+enum MIValueType {
+ /// ident
+ empty,
+ /// ident
+ ident,
+ /// c-string
+ str,
+ /// key=value pair
+ keyValue,
+ /// list []
+ list,
+ /// map {key=value, ...}
+ map,
+}
+
+class MIValue {
+ MIValueType type;
+ this(MIValueType type) {
+ this.type = type;
+ }
+ @property string str() { return null; }
+ @property int length() { return 1; }
+ MIValue opIndex(int index) { return null; }
+ MIValue opIndex(string key) { return null; }
+}
+
+class MIKeyValue : MIValue {
+ private string _key;
+ private MIValue _value;
+ this(string key, MIValue value) {
+ super(MIValueType.keyValue);
+ _key = key;
+ _value = value;
+ }
+ @property string key() { return _key; }
+ override @property string str() { return _key; }
+ @property MIValue value() { return _value; }
+}
+
+class MIIdent : MIValue {
+ private string _ident;
+ this(string ident) {
+ super(MIValueType.ident);
+ _ident = ident;
+ }
+ override @property string str() { return _ident; }
+}
+
+class MIString : MIValue {
+ private string _str;
+ this(string str) {
+ super(MIValueType.str);
+ _str = str;
+ }
+ override @property string str() { return _str; }
+}
+
+class MIList : MIValue {
+ private MIValue[] _items;
+ private MIValue[string] _map;
+
+ override @property int length() { return cast(int)_items.length; }
+ override MIValue opIndex(int index) {
+ if (index < 0 || index >= _items.length)
+ return null;
+ return _items[index];
+ }
+
+ override MIValue opIndex(string key) {
+ if (key in _map) {
+ MIValue res = _map[key];
+ return res;
+ }
+ return null;
+ }
+
+ this(MIValue[] items) {
+ super(MIValueType.list);
+ _items = items;
+ // fill map
+ foreach(item; _items) {
+ if (item.type == MIValueType.keyValue) {
+ if (!item.str.empty)
+ _map[item.str] = (cast(MIKeyValue)item).value;
+ }
+ }
+ }
+}
+
+class MIMap : MIList {
+ this(MIValue[] items) {
+ super(items);
+ type = MIValueType.map;
+ }
+}
+
+private char nextChar(ref string s) {
+ if (s.empty)
+ return 0;
+ char ch = s[0];
+ s = s[1 .. $];
+ return ch;
+}
+
+string parseCString(ref string s) {
+ char[] res;
+ // skip opening "
+ char ch = nextChar(s);
+ if (!ch)
+ return null;
+ s = s[1 .. $];
+ if (ch != '\"')
+ return null;
+ for (;;) {
+ if (s.empty) {
+ // unexpected end of string
+ return null;
+ }
+ ch = nextChar(s);
+ if (ch == '\"')
+ break;
+ if (ch == '\\') {
+ // escape sequence
+ ch = nextChar(s);
+ if (ch >= '0' && ch <= '7') {
+ // octal
+ int number = (ch - '0');
+ char ch2 = nextChar(s);
+ char ch3 = nextChar(s);
+ if (ch2 < '0' || ch2 > '7')
+ return null;
+ if (ch3 < '0' || ch3 > '7')
+ return null;
+ number = number * 8 + (ch2 - '0');
+ number = number * 8 + (ch3 - '0');
+ if (number > 255)
+ return null; // invalid octal number
+ res ~= cast(char)number;
+ } else {
+ switch (ch) {
+ case 'n':
+ res ~= '\n';
+ break;
+ case 'r':
+ res ~= '\r';
+ break;
+ case 't':
+ res ~= '\t';
+ break;
+ case 'a':
+ res ~= '\a';
+ break;
+ case 'b':
+ res ~= '\b';
+ break;
+ case 'f':
+ res ~= '\f';
+ break;
+ case 'v':
+ res ~= '\v';
+ break;
+ case 'x': {
+ // 2 digit hex number
+ uint ch2 = decodeHexDigit(nextChar(s));
+ uint ch3 = decodeHexDigit(nextChar(s));
+ if (ch2 > 15 || ch3 > 15)
+ return null;
+ res ~= cast(char)((ch2 << 4) | ch3);
+ break;
+ }
+ default:
+ res ~= ch;
+ break;
+ }
+ }
+ } else {
+ res ~= ch;
+ }
+ }
+ if (!res.length)
+ return "";
+ return res.dup;
+}
+
+/// decodes hex digit (0..9, a..f, A..F), returns uint.max if invalid
+private uint decodeHexDigit(T)(T ch) {
+ if (ch >= '0' && ch <= '9')
+ return ch - '0';
+ else if (ch >= 'a' && ch <= 'f')
+ return ch - 'a' + 10;
+ else if (ch >= 'A' && ch <= 'F')
+ return ch - 'A' + 10;
+ return uint.max;
+}
+