1 module ddebug.gdb.gdbmiparser; 2 3 import dlangui.core.logger; 4 import std.utf; 5 import std.conv : to; 6 import std.array : empty; 7 import std.algorithm : startsWith, equal; 8 import std..string : format; 9 import ddebug.common.debugger; 10 11 /// result class 12 enum ResultClass { 13 done, 14 running, 15 connected, 16 error, 17 exit, 18 other 19 } 20 21 /// async message class 22 enum AsyncClass { 23 running, 24 stopped, 25 library_loaded, 26 library_unloaded, 27 thread_group_added, 28 thread_group_started, 29 thread_group_exited, 30 thread_created, 31 thread_exited, 32 other 33 } 34 35 /// Parse GDB MI output string 36 MIValue parseMI(string s) { 37 string src = s; 38 try { 39 bool err = false; 40 //Log.e("Tokenizing MI output: " ~ src); 41 MIToken[] tokens = tokenizeMI(s, err); 42 if (err) { 43 // tokenizer error 44 Log.e("Cannot tokenize MI output `" ~ src ~ "`"); 45 return null; 46 } 47 //Log.v("Parsing tokens " ~ tokens.dumpTokens); 48 MIValue[] items = parseMIList(tokens); 49 //Log.v("Found " ~ to!string(items.length) ~ " values in list"); 50 return new MIList(items); 51 } catch (Exception e) { 52 Log.e("Cannot parse MI output `" ~ src ~ "`", e.msg); 53 return null; 54 } 55 } 56 57 string demangleFunctionName(string mangledName) { 58 import std.ascii; 59 if (!mangledName) 60 return mangledName; 61 string fn = mangledName; 62 if (!fn.startsWith("_D")) { 63 // trying to fix strange corrupted mangling under OSX/dmd/lldb 64 if (fn.length < 3 || fn[0]!='D' || !isDigit(fn[1])) 65 return mangledName; 66 fn = "_" ~ mangledName; 67 } 68 import std.demangle; 69 import std.ascii; 70 //import core.demangle : Demangle; 71 uint i = 0; 72 for(; i < fn.length && (fn[i] == '_' || isAlphaNum(fn[i])); i++) { 73 // next 74 } 75 string rest = i < fn.length ? fn[i .. $] : null; 76 try { 77 return demangle(fn[0..i]) ~ rest; 78 } catch (Exception e) { 79 // cannot demangle 80 Log.v("Failed to demangle " ~ fn[0..i]); 81 return mangledName; 82 } 83 } 84 85 /* 86 frame = { 87 addr = "0x00000000004015b2", 88 func = "D main", 89 args = [], 90 file = "source\app.d", 91 fullname = "D:\projects\d\dlangide\workspaces\helloworld\helloworld/source\app.d", 92 line = "8" 93 }, 94 */ 95 DebugFrame parseFrame(MIValue frame) { 96 import std.path; 97 if (!frame) 98 return null; 99 DebugFrame location = new DebugFrame(); 100 location.file = baseName(toNativeDelimiters(frame.getString("file"))); 101 location.projectFilePath = toNativeDelimiters(frame.getString("file")); 102 location.fullFilePath = toNativeDelimiters(frame.getString("fullname")); 103 location.line = frame.getInt("line"); 104 location.func = demangleFunctionName(frame.getString("func")); 105 location.address = frame.getUlong("addr"); 106 location.from = toNativeDelimiters(frame.getString("from")); 107 location.level = frame.getInt("level"); 108 return location; 109 } 110 111 DebugVariableList parseVariableList(MIValue params, string paramName = "variables") { 112 if (!params) 113 return null; 114 DebugVariableList res = new DebugVariableList(); 115 MIValue list = params[paramName]; 116 if (list && list.isList) { 117 for(int i = 0; i < list.length; i++) { 118 if (DebugVariable t = parseVariable(list[i])) 119 res.variables ~= t; 120 } 121 } 122 return res; 123 } 124 125 DebugVariable parseVariable(MIValue params) { 126 if (!params) 127 return null; 128 DebugVariable res = new DebugVariable(); 129 res.name = params.getString("name"); 130 res.value = params.getString("value"); 131 res.type = params.getString("type"); 132 return res; 133 } 134 135 DebugThreadList parseThreadList(MIValue params) { 136 if (!params) 137 return null; 138 DebugThreadList res = new DebugThreadList(); 139 res.currentThreadId = params.getUlong("current-thread-id"); 140 MIValue threads = params["threads"]; 141 if (threads && threads.isList) { 142 for(int i = 0; i < threads.length; i++) { 143 if (DebugThread t = parseThread(threads[i])) 144 res.threads ~= t; 145 } 146 } 147 return res; 148 } 149 150 DebugThread parseThread(MIValue params) { 151 if (!params) 152 return null; 153 DebugThread res = new DebugThread(); 154 res.id = params.getUlong("id"); 155 res.name = params.getString("target-id"); 156 if (res.name.empty) 157 res.name = "Thread%d".format(res.id); 158 string stateName = params.getString("state"); 159 if (stateName == "stopped") 160 res.state = DebuggingState.paused; 161 else if (stateName == "running") 162 res.state = DebuggingState.running; 163 else 164 res.state = DebuggingState.stopped; 165 res.frame = parseFrame(params["frame"]); 166 return res; 167 } 168 169 DebugStack parseStack(MIValue params) { 170 if (!params) 171 return null; 172 MIValue stack = params["stack"]; 173 if (!stack) 174 return null; 175 DebugStack res = new DebugStack(); 176 for (int i = 0; i < stack.length; i++) { 177 MIValue item = stack[i]; 178 if (item && item.isKeyValue && item.key.equal("frame")) { 179 DebugFrame location = parseFrame(item.value); 180 if (location) { 181 res.frames ~= location; 182 } 183 } 184 } 185 return res; 186 } 187 188 string toNativeDelimiters(string s) { 189 version(Windows) { 190 char[] buf; 191 foreach(ch; s) { 192 if (ch == '/') 193 buf ~= '\\'; 194 else 195 buf ~= ch; 196 } 197 return buf.dup; 198 } else { 199 return s; 200 } 201 } 202 203 string parseIdent(ref string s) { 204 string res = null; 205 int len = 0; 206 for(; len < s.length; len++) { 207 char ch = s[len]; 208 if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-')) 209 break; 210 } 211 if (len > 0) { 212 res = s[0..len]; 213 s = s[len .. $]; 214 } 215 return res; 216 } 217 218 bool skipComma(ref string s) { 219 if (s.length > 0 && s[0] == ',') { 220 s = s[1 .. $]; 221 return true; 222 } 223 return false; 224 } 225 226 string parseIdentAndSkipComma(ref string s) { 227 string res = parseIdent(s); 228 skipComma(s); 229 return res; 230 } 231 232 ResultClass resultByName(string s) { 233 if (s.equal("done")) return ResultClass.done; 234 if (s.equal("running")) return ResultClass.running; 235 if (s.equal("connected")) return ResultClass.connected; 236 if (s.equal("error")) return ResultClass.error; 237 if (s.equal("exit")) return ResultClass.exit; 238 return ResultClass.other; 239 } 240 241 AsyncClass asyncByName(string s) { 242 if (s.equal("stopped")) return AsyncClass.stopped; 243 if (s.equal("running")) return AsyncClass.running; 244 if (s.equal("library-loaded")) return AsyncClass.library_loaded; 245 if (s.equal("library-unloaded")) return AsyncClass.library_unloaded; 246 if (s.equal("thread-group-added")) return AsyncClass.thread_group_added; 247 if (s.equal("thread-group-started")) return AsyncClass.thread_group_started; 248 if (s.equal("thread-group-exited")) return AsyncClass.thread_group_exited; 249 if (s.equal("thread-created")) return AsyncClass.thread_created; 250 if (s.equal("thread-exited")) return AsyncClass.thread_exited; 251 return AsyncClass.other; 252 } 253 254 enum MITokenType { 255 /// end of line 256 eol, 257 /// error 258 error, 259 /// identifier 260 ident, 261 /// C string 262 str, 263 /// = sign 264 eq, 265 /// , sign 266 comma, 267 /// { brace 268 curlyOpen, 269 /// } brace 270 curlyClose, 271 /// [ brace 272 squareOpen, 273 /// ] brace 274 squareClose, 275 } 276 277 struct MIToken { 278 MITokenType type; 279 string str; 280 this(MITokenType type, string str = null) { 281 this.type = type; 282 this.str = str; 283 } 284 string toString() { 285 //return type == MITokenType.str ? to!string(type) ~ ":\"" ~ str ~ "\"": to!string(type) ~ ":" ~ str; 286 return (type == MITokenType.str) ? "\"" ~ str ~ "\"" : str; 287 } 288 } 289 290 MIToken parseMIToken(ref string s) { 291 if (s.empty) 292 return MIToken(MITokenType.eol); 293 char ch = s[0]; 294 if (ch == ',') { 295 s = s[1..$]; 296 return MIToken(MITokenType.comma, ","); 297 } 298 if (ch == '=') { 299 s = s[1..$]; 300 return MIToken(MITokenType.eq, "="); 301 } 302 if (ch == '{') { 303 s = s[1..$]; 304 return MIToken(MITokenType.curlyOpen, "{"); 305 } 306 if (ch == '}') { 307 s = s[1..$]; 308 return MIToken(MITokenType.curlyClose, "}"); 309 } 310 if (ch == '[') { 311 s = s[1..$]; 312 return MIToken(MITokenType.squareOpen, "["); 313 } 314 if (ch == ']') { 315 s = s[1..$]; 316 return MIToken(MITokenType.squareClose, "]"); 317 } 318 // C string 319 if (ch == '\"') { 320 string str = parseCString(s); 321 if (!str.ptr) { 322 return MIToken(MITokenType.error); 323 } 324 return MIToken(MITokenType.str, str); 325 } 326 // identifier 327 string str = parseIdent(s); 328 if (!str.empty) 329 return MIToken(MITokenType.ident, str); 330 return MIToken(MITokenType.error); 331 } 332 333 /// tokenize GDB MI output into array of tokens 334 MIToken[] tokenizeMI(string s, out bool error) { 335 error = false; 336 string src = s; 337 MIToken[] res; 338 for(;;) { 339 MIToken token = parseMIToken(s); 340 if (token.type == MITokenType.eol) 341 break; 342 if (token.type == MITokenType.error) { 343 error = true; 344 Log.e("Error while tokenizing GDB output ", src, " near ", s); 345 break; 346 } 347 res ~= token; 348 } 349 return res; 350 } 351 352 string dumpTokens(MIToken[] tokens, int maxTokens = 40, int maxChars = 80) { 353 char[] buf; 354 for (int i = 0; i < maxTokens && i < tokens.length && buf.length < maxChars; i++) { 355 buf ~= tokens[i].toString; 356 buf ~= ' '; 357 } 358 return buf.dup; 359 } 360 361 MIValue parseMIValue(ref MIToken[] tokens) { 362 MIToken[] srctokens; 363 if (tokens.length == 0) 364 throw new Exception("parseMIValue: Unexpected end of line when value is expected"); 365 MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol; 366 MITokenType nextTokenType = tokens.length > 1 ? tokens[1].type : MITokenType.eol; 367 if (tokenType == MITokenType.ident) { 368 string ident = tokens[0].str; 369 if (nextTokenType == MITokenType.eol || nextTokenType == MITokenType.comma) { 370 MIValue res = new MIIdent(ident); 371 tokens = tokens[1..$]; 372 return res; 373 } else if (nextTokenType == MITokenType.eq) { 374 tokens = tokens[1..$]; // skip ident 375 tokens = tokens[1..$]; // skip = 376 MIValue value = parseMIValue(tokens); 377 //tokens = tokens[1..$]; // skip value 378 MIValue res = new MIKeyValue(ident, value); 379 return res; 380 } 381 throw new Exception("parseMIValue: Unexpected token " ~ tokens[0].toString ~ " near " ~ srctokens.dumpTokens); 382 } else if (tokenType == MITokenType.str) { 383 string str = tokens[0].str; 384 tokens = tokens[1..$]; 385 return new MIString(str); 386 } else if (tokenType == MITokenType.curlyOpen) { 387 tokens = tokens[1..$]; 388 MIValue[] list = parseMIList(tokens, MITokenType.curlyClose); 389 return new MIMap(list); 390 } else if (tokenType == MITokenType.squareOpen) { 391 tokens = tokens[1..$]; 392 MIValue[] list = parseMIList(tokens, MITokenType.squareClose); 393 return new MIList(list); 394 } 395 throw new Exception("parseMIValue: unexpected token " ~ tokens[0].toString ~ " near " ~ srctokens.dumpTokens); 396 } 397 398 private MIValue[] parseMIList(ref MIToken[] tokens, MITokenType closingToken = MITokenType.eol) { 399 //Log.v("parseMIList: " ~ tokens.dumpTokens); 400 MIValue[] res; 401 if (!tokens.length) 402 return res; 403 for (;;) { 404 MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol; 405 if (tokenType == closingToken) { 406 if (tokenType != MITokenType.eol) 407 tokens = tokens[1..$]; 408 return res; 409 } 410 if (tokenType == MITokenType.eol) 411 throw new Exception("parseMIList: Unexpected eol in list"); 412 if (res.length > 0) { 413 // comma required 414 if (tokenType != MITokenType.comma) 415 throw new Exception("parseMIList: comma expected, found " ~ tokens[0].toString ~ " near " ~ tokens.dumpTokens); 416 tokens = tokens[1..$]; 417 } 418 MIValue value = parseMIValue(tokens); 419 res ~= value; 420 } 421 } 422 423 enum MIValueType { 424 /// ident 425 empty, 426 /// ident 427 ident, 428 /// c-string 429 str, 430 /// key=value pair 431 keyValue, 432 /// list [] 433 list, 434 /// map {key=value, ...} 435 map, 436 } 437 438 private void dumpLevel(ref char[] buf, int level) { 439 for (int i = 0; i < level; i++) 440 buf ~= " "; 441 } 442 443 class MIValue { 444 MIValueType type; 445 this(MIValueType type) { 446 this.type = type; 447 } 448 @property string str() { return null; } 449 @property int length() { return 0; } 450 MIValue opIndex(int index) { return null; } 451 MIValue opIndex(string key) { return null; } 452 @property bool isIdent() { return type == MIValueType.list; } 453 @property bool isString() { return type == MIValueType.str; } 454 @property bool isKeyValue() { return type == MIValueType.keyValue; } 455 @property bool isMap() { return type == MIValueType.map; } 456 @property bool isList() { return type == MIValueType.list; } 457 @property string key() { return str; } 458 @property MIValue value() { return this; } 459 460 string getString(string name) { 461 MIValue v = opIndex(name); 462 if (!v) 463 return null; 464 return v.str; 465 } 466 467 int getInt(string name, int defValue = 0) { 468 MIValue v = opIndex(name); 469 if (!v) 470 return defValue; 471 string s = v.str; 472 if (s.empty) 473 return defValue; 474 return cast(int)decodeNumber(s, defValue); 475 } 476 477 ulong getUlong(string name, ulong defValue = 0) { 478 MIValue v = opIndex(name); 479 if (!v) 480 return defValue; 481 string s = v.str; 482 if (s.empty) 483 return defValue; 484 return decodeNumber(s, defValue); 485 } 486 487 string getString(int index) { 488 MIValue v = opIndex(index); 489 if (!v) 490 return null; 491 return v.str; 492 } 493 494 void dump(ref char[] buf, int level) { 495 //dumpLevel(buf, level); 496 buf ~= str; 497 } 498 override string toString() { 499 char[] buf; 500 dump(buf, 0); 501 return buf.dup; 502 } 503 } 504 505 class MIKeyValue : MIValue { 506 private string _key; 507 private MIValue _value; 508 this(string key, MIValue value) { 509 super(MIValueType.keyValue); 510 _key = key; 511 _value = value; 512 } 513 override @property string key() { return _key; } 514 override @property MIValue value() { return _value; } 515 override @property string str() { return _key; } 516 override void dump(ref char[] buf, int level) { 517 //dumpLevel(buf, level); 518 buf ~= _key; 519 buf ~= " = "; 520 if (!value) 521 buf ~= "null"; 522 else 523 _value.dump(buf, level + 1); 524 } 525 } 526 527 class MIIdent : MIValue { 528 private string _ident; 529 this(string ident) { 530 super(MIValueType.ident); 531 _ident = ident; 532 } 533 override @property string str() { return _ident; } 534 } 535 536 class MIString : MIValue { 537 private string _str; 538 this(string str) { 539 super(MIValueType.str); 540 _str = str; 541 } 542 override @property string str() { return _str; } 543 override void dump(ref char[] buf, int level) { 544 //dumpLevel(buf, level); 545 buf ~= '\"'; 546 buf ~= str; 547 buf ~= '\"'; 548 } 549 } 550 551 class MIList : MIValue { 552 private MIValue[] _items; 553 private MIValue[string] _map; 554 555 override @property int length() { return cast(int)_items.length; } 556 override MIValue opIndex(int index) { 557 if (index < 0 || index >= _items.length) 558 return null; 559 return _items[index]; 560 } 561 562 override MIValue opIndex(string key) { 563 if (key in _map) { 564 MIValue res = _map[key]; 565 return res; 566 } 567 return null; 568 } 569 570 this(MIValue[] items) { 571 super(MIValueType.list); 572 _items = items; 573 // fill map 574 foreach(item; _items) { 575 if (item.type == MIValueType.keyValue) { 576 if (!item.str.empty) 577 _map[item.str] = (cast(MIKeyValue)item).value; 578 } 579 } 580 } 581 override void dump(ref char[] buf, int level) { 582 buf ~= (type == MIValueType.map) ? "{" : "["; 583 if (length) { 584 buf ~= "\n"; 585 for (int i = 0; i < length; i++) { 586 dumpLevel(buf, level + 1); 587 _items[i].dump(buf, level + 1); 588 if (i < length - 1) 589 buf ~= ","; 590 buf ~= "\n"; 591 } 592 //buf ~= "\n"; 593 dumpLevel(buf, level - 1); 594 } 595 buf ~= (type == MIValueType.map) ? "}" : "]"; 596 } 597 } 598 599 class MIMap : MIList { 600 this(MIValue[] items) { 601 super(items); 602 type = MIValueType.map; 603 } 604 } 605 606 private char nextChar(ref string s) { 607 if (s.empty) 608 return 0; 609 char ch = s[0]; 610 s = s[1 .. $]; 611 return ch; 612 } 613 614 string parseCString(ref string s) { 615 char[] res; 616 // skip opening " 617 char ch = nextChar(s); 618 if (!ch) 619 return null; 620 if (ch != '\"') 621 return null; 622 for (;;) { 623 if (s.empty) { 624 // unexpected end of string 625 return null; 626 } 627 ch = nextChar(s); 628 if (ch == '\"') 629 break; 630 if (ch == '\\') { 631 // escape sequence 632 ch = nextChar(s); 633 if (ch >= '0' && ch <= '7') { 634 // octal 635 int number = (ch - '0'); 636 char ch2 = nextChar(s); 637 char ch3 = nextChar(s); 638 if (ch2 < '0' || ch2 > '7') 639 return null; 640 if (ch3 < '0' || ch3 > '7') 641 return null; 642 number = number * 8 + (ch2 - '0'); 643 number = number * 8 + (ch3 - '0'); 644 if (number > 255) 645 return null; // invalid octal number 646 res ~= cast(char)number; 647 } else { 648 switch (ch) { 649 case 'n': 650 res ~= '\n'; 651 break; 652 case 'r': 653 res ~= '\r'; 654 break; 655 case 't': 656 res ~= '\t'; 657 break; 658 case 'a': 659 res ~= '\a'; 660 break; 661 case 'b': 662 res ~= '\b'; 663 break; 664 case 'f': 665 res ~= '\f'; 666 break; 667 case 'v': 668 res ~= '\v'; 669 break; 670 case 'x': { 671 // 2 digit hex number 672 uint ch2 = decodeHexDigit(nextChar(s)); 673 uint ch3 = decodeHexDigit(nextChar(s)); 674 if (ch2 > 15 || ch3 > 15) 675 return null; 676 res ~= cast(char)((ch2 << 4) | ch3); 677 break; 678 } 679 default: 680 res ~= ch; 681 break; 682 } 683 } 684 } else { 685 res ~= ch; 686 } 687 } 688 if (!res.length) 689 return ""; 690 return res.dup; 691 } 692 693 /// decodes hex digit (0..9, a..f, A..F), returns uint.max if invalid 694 private uint decodeHexDigit(T)(T ch) { 695 if (ch >= '0' && ch <= '9') 696 return ch - '0'; 697 else if (ch >= 'a' && ch <= 'f') 698 return ch - 'a' + 10; 699 else if (ch >= 'A' && ch <= 'F') 700 return ch - 'A' + 10; 701 return uint.max; 702 } 703 704 private ulong decodeNumber(string s, ulong defValue) { 705 if (s.empty) 706 return defValue; 707 if (s.length > 2 && s.startsWith("0x")) { 708 s = s[2..$]; 709 ulong res = 0; 710 foreach(ch; s) { 711 uint digit = decodeHexDigit(ch); 712 if (digit > 15) 713 return defValue; 714 res = res * 16 + digit; 715 } 716 return res; 717 } else { 718 return to!ulong(s); 719 } 720 }