/******************************************************************************* * Copyright (c) 2000, 2009 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * QNX Software Systems - Initial API and implementation *******************************************************************************/ package org.eclipse.cdt.debug.mi.core.output; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; /** <pre> `OUTPUT :' `( OUT-OF-BAND-RECORD )* [ RESULT-RECORD ] "(gdb)" NL' `RESULT-RECORD :' ` [ TOKEN ] "^" RESULT-CLASS ( "," RESULT )* NL' `OUT-OF-BAND-RECORD :' `ASYNC-RECORD | STREAM-RECORD' `ASYNC-RECORD :' `EXEC-ASYNC-OUTPUT | STATUS-ASYNC-OUTPUT | NOTIFY-ASYNC-OUTPUT' `EXEC-ASYNC-OUTPUT :' `[ TOKEN ] "*" ASYNC-OUTPUT' `STATUS-ASYNC-OUTPUT :' `[ TOKEN ] "+" ASYNC-OUTPUT' `NOTIFY-ASYNC-OUTPUT :' `[ TOKEN ] "=" ASYNC-OUTPUT' `ASYNC-OUTPUT :' `ASYNC-CLASS ( "," RESULT )* NL' `RESULT-CLASS :' `"done" | "running" | "connected" | "error" | "exit"' `ASYNC-CLASS :' `"stopped" | OTHERS' (where OTHERS will be added depending on the needs--this is still in development). `RESULT :' ` VARIABLE "=" VALUE' `VARIABLE :' ` STRING ' `VALUE :' ` CONST | TUPLE | LIST ' `CONST :' `C-STRING' `TUPLE :' ` "{}" | "{" RESULT ( "," RESULT )* "}" ' `LIST :' ` "[]" | "[" VALUE ( "," VALUE )* "]" | "[" RESULT ( "," RESULT )* "]" ' `STREAM-RECORD :' `CONSOLE-STREAM-OUTPUT | TARGET-STREAM-OUTPUT | LOG-STREAM-OUTPUT' `CONSOLE-STREAM-OUTPUT :' `"~" C-STRING' `TARGET-STREAM-OUTPUT :' `"@" C-STRING' `LOG-STREAM-OUTPUT :' `"&" C-STRING' `NL :' `CR | CR-LF' `TOKEN :' _any sequence of digits_. `C-STRING :' `""" SEVEN-BIT-ISO-C-STRING-CONTENT """' </pre> */ public class MIParser { public String primaryPrompt = "(gdb)"; //$NON-NLS-1$ public String cliPrompt = primaryPrompt; public String secondaryPrompt = ">"; //$NON-NLS-1$ /** * Point of entry to create an AST for MI. * * @param buffer Output from MI Channel. * @return MIOutput * @see MIOutput */ public MIOutput parse(String buffer) { MIOutput mi = new MIOutput(); MIResultRecord rr = null; List oobs = new ArrayList(1); int id = -1; StringTokenizer st = new StringTokenizer(buffer, "\n"); //$NON-NLS-1$ while (st.hasMoreTokens()) { StringBuffer token = new StringBuffer(st.nextToken()); // Fetch the Token/Id if (token.length() > 0 && Character.isDigit(token.charAt(0))) { int i = 1; while (i < token.length() && Character.isDigit(token.charAt(i))) { i++; } String numbers = token.substring(0, i); try { id = Integer.parseInt(numbers); } catch (NumberFormatException e) { } // Consume the token. token.delete(0, i); } // ResultRecord ||| Out-Of-Band Records if (token.length() > 0) { if (token.charAt(0) == '^') { token.deleteCharAt(0); rr = processMIResultRecord(token, id); } else if (startsWith(token, primaryPrompt)) { //break; // Do nothing. } else { MIOOBRecord band = processMIOOBRecord(token, id); if (band != null) { oobs.add(band); } } } } MIOOBRecord[] bands = (MIOOBRecord[]) oobs.toArray(new MIOOBRecord[oobs.size()]); mi.setMIOOBRecords(bands); mi.setMIResultRecord(rr); return mi; } /** * Assuming '^' was deleted from the Result Record. */ private MIResultRecord processMIResultRecord(StringBuffer buffer, int id) { MIResultRecord rr = new MIResultRecord(); rr.setToken(id); if (buffer.toString().startsWith(MIResultRecord.DONE)) { rr.setResultClass(MIResultRecord.DONE); buffer.delete(0, MIResultRecord.DONE.length()); } else if (buffer.toString().startsWith(MIResultRecord.ERROR)) { rr.setResultClass(MIResultRecord.ERROR); buffer.delete(0, MIResultRecord.ERROR.length()); } else if (buffer.toString().startsWith(MIResultRecord.EXIT)) { rr.setResultClass(MIResultRecord.EXIT); buffer.delete(0, MIResultRecord.EXIT.length()); } else if (buffer.toString().startsWith(MIResultRecord.RUNNING)) { rr.setResultClass(MIResultRecord.RUNNING); buffer.delete(0, MIResultRecord.RUNNING.length()); } else if (buffer.toString().startsWith(MIResultRecord.CONNECTED)) { rr.setResultClass(MIResultRecord.CONNECTED); buffer.delete(0, MIResultRecord.CONNECTED.length()); } else { // FIXME: // Error throw an exception? } // Results are separated by commas. if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); MIResult[] res = processMIResults(new FSB(buffer)); rr.setMIResults(res); } return rr; } /** * Find OutOfBand Records depending on the starting token. */ private MIOOBRecord processMIOOBRecord(StringBuffer buffer, int id) { MIOOBRecord oob = null; char c = buffer.charAt(0); if (c == '*' || c == '+' || c == '=') { // Consume the first char buffer.deleteCharAt(0); MIAsyncRecord async = null; switch (c) { case '*' : async = new MIExecAsyncOutput(); break; case '+' : async = new MIStatusAsyncOutput(); break; case '=' : async = new MINotifyAsyncOutput(); break; } async.setToken(id); // Extract the Async-Class int i = buffer.toString().indexOf(','); if (i != -1) { String asyncClass = buffer.substring(0, i); async.setAsyncClass(asyncClass); // Consume the async-class and the comma buffer.delete(0, i + 1); } else { async.setAsyncClass(buffer.toString().trim()); buffer.setLength(0); } MIResult[] res = processMIResults(new FSB(buffer)); async.setMIResults(res); oob = async; } else if (c == '~' || c == '@' || c == '&') { // Consume the first char buffer.deleteCharAt(0); MIStreamRecord stream = null; switch (c) { case '~' : stream = new MIConsoleStreamOutput(); break; case '@' : stream = new MITargetStreamOutput(); break; case '&' : stream = new MILogStreamOutput(); break; } stream.setCString(removeSurroundingDoubleQuotes(buffer.toString())); oob = stream; } else { // Badly format MI line, just pass it to the user as target stream MIStreamRecord stream = new MITargetStreamOutput(); String res = buffer.toString(); // this awfull expression just mean to replace \ with \\. This is needed because otherwise escaping is lost. // this is to fix bug 255946 without breaking other stuff 286785 res = res.replaceAll("\\Q\\", "\\\\\\\\"); //$NON-NLS-1$//$NON-NLS-2$ stream.setCString(res + "\n"); //$NON-NLS-1$ oob = stream; } return oob; } private String removeSurroundingDoubleQuotes(String str) { String s = str; // remove leading double quote if (s.startsWith("\"")) { //$NON-NLS-1$ s = s.substring(1); } // remove trailing double quote if (s.endsWith("\"")) { //$NON-NLS-1$ s = s.substring(0, s.length() - 1); } return s; } /** * Assuming that the usual leading comma was consumed. * Extract the MI Result comma separated responses. */ private MIResult[] processMIResults(FSB buffer) { List aList = new ArrayList(); MIResult result = processMIResult(buffer); if (result != null) { aList.add(result); } while (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); result = processMIResult(buffer); if (result != null) { aList.add(result); } } return (MIResult[]) aList.toArray(new MIResult[aList.size()]); } /** * Construct the MIResult. Characters will be consume/delete * moving forward constructing the AST. */ private MIResult processMIResult(FSB buffer) { MIResult result = new MIResult(); int equal; if (buffer.length() > 0 && Character.isLetter(buffer.charAt(0)) && (equal = buffer.indexOf('=')) != -1) { String variable = buffer.substring(0, equal); result.setVariable(variable); buffer.delete(0, equal + 1); MIValue value = processMIValue(buffer); result.setMIValue(value); } else if(buffer.length()>0 && buffer.charAt(0)=='"') { // This an error but we just swallow it and move on. MIValue value = processMIValue(buffer); result.setMIValue(value); } else { result.setVariable(buffer.toString()); result.setMIValue(new MIConst()); // Empty string:??? buffer.setLength(0); } return result; } /** * Find a MIValue implementation or return null. */ private MIValue processMIValue(FSB buffer) { MIValue value = null; if (buffer.length() > 0) { if (buffer.charAt(0) == '{') { buffer.deleteCharAt(0); value = processMITuple(buffer); } else if (buffer.charAt(0) == '[') { buffer.deleteCharAt(0); value = processMIList(buffer); } else if (buffer.charAt(0) == '"') { buffer.deleteCharAt(0); MIConst cnst = new MIConst(); cnst.setCString(translateCString(buffer)); value = cnst; } } return value; } /** * Assuming the starting '{' was deleted form the StringBuffer, * go to the closing '}' consuming/deleting all the characters. * This is usually call by processMIvalue(); */ private MIValue processMITuple(FSB buffer) { MITuple tuple = new MITuple(); List valueList = new ArrayList(); List resultList = new ArrayList(); // Catch closing '}' while (buffer.length() > 0 && buffer.charAt(0) != '}') { // Try for the MIValue first MIValue value = processMIValue(buffer); if (value != null) { valueList.add(value); } else { MIResult result = processMIResult(buffer); if (result != null) { resultList.add(result); } } if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); } } if (buffer.length() > 0 && buffer.charAt(0) == '}') { buffer.deleteCharAt(0); } MIValue[] values = (MIValue[]) valueList.toArray(new MIValue[valueList.size()]); MIResult[] res = (MIResult[]) resultList.toArray(new MIResult[resultList.size()]); tuple.setMIValues(values); tuple.setMIResults(res); return tuple; } /** * Assuming the leading '[' was deleted, find the closing * ']' consuming/delete chars from the StringBuffer. */ private MIValue processMIList(FSB buffer) { MIList list = new MIList(); List valueList = new ArrayList(); List resultList = new ArrayList(); // catch closing ']' while (buffer.length() > 0 && buffer.charAt(0) != ']') { // Try for the MIValue first MIValue value = processMIValue(buffer); if (value != null) { valueList.add(value); } else { MIResult result = processMIResult(buffer); if (result != null) { resultList.add(result); } } if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); } } if (buffer.length() > 0 && buffer.charAt(0) == ']') { buffer.deleteCharAt(0); } MIValue[] values = (MIValue[]) valueList.toArray(new MIValue[valueList.size()]); MIResult[] res = (MIResult[]) resultList.toArray(new MIResult[resultList.size()]); list.setMIValues(values); list.setMIResults(res); return list; } /* * MI C-String rather MIConst values are enclose in double quotes * and any double quotes or backslash in the string are escaped. * Assuming the starting double quote was removed. * This method will stop at the closing double quote remove the extra * backslash escaping and return the string __without__ the enclosing double quotes * The original StringBuffer will move forward. */ private String translateCString(FSB buffer) { boolean escape = false; boolean closingQuotes = false; StringBuffer sb = new StringBuffer(); int index = 0; for (; index < buffer.length() && !closingQuotes; index++) { char c = buffer.charAt(index); if (c == '\\') { if (escape) { sb.append(c); escape = false; } else { escape = true; } } else if (c == '"') { if (escape) { sb.append(c); escape = false; } else { // Bail out. closingQuotes = true; } } else { if (escape) { sb.append('\\'); } sb.append(c); escape = false; } } buffer.delete(0, index); return sb.toString(); } /** * Tests if this string starts with the specified prefix beginning * a specified index. * * @param value the string. * @param prefix the prefix. * @return <code>true</code> if prefix starts value. */ public boolean startsWith(StringBuffer value, String prefix) { int vlen = value.length(); int plen = prefix.length(); if (vlen < plen) { return false; } for (int i = 0; i < plen; i++) { if (value.charAt(i) != prefix.charAt(i)) { return false; } } return true; } /** * Fast String Buffer class. MIParser does a lot * of deleting off the front of a string, that's clearly * an order N operation for StringBuffer which makes * the MIParser an order N^2 operation. There are "issues" * with this for large arrays. Use of FSB rather than String * Buffer makes MIParser N rather than N^2 because FSB can * delete from the front in constant time. */ public class FSB { StringBuffer buf; int pos; boolean shared; public FSB(StringBuffer buf) { this.buf = buf; pos = 0; shared = false; } public FSB(FSB fbuf) { pos = fbuf.pos; buf = fbuf.buf; shared = true; } public int length() { int res = buf.length() - pos; if (res < 0) return 0; return res; } public char charAt(int index) { return buf.charAt(index + pos); } private void resolveCopy() { if (shared) { buf = new StringBuffer(buf.toString()); shared = false; } } public FSB deleteCharAt(int index) { if (index == 0) { pos++; } else { resolveCopy(); buf = buf.deleteCharAt(pos + index); } return this; } public FSB delete(int start, int end) { if (start == 0) { pos = pos + end - start; } else { resolveCopy(); buf.delete(start + pos, end + pos); } return this; } public void setLength(int a) { if (a == 0) pos = buf.length(); else { // panic! fortunately we don't do this. } } public String substring(int start, int end) { return buf.substring(start + pos, end + pos); } public String toString() { return buf.substring(pos, buf.length()); } int indexOf(char c) { int len = buf.length(); for (int i = pos; i < len; i++) { if (buf.charAt(i) == c) return i - pos; } return -1; } boolean startsWith(String s) { int len = Math.min(s.length(), length()); if (len < s.length()) return false; for (int i = 0; i < len; i++) { if (s.charAt(i) != buf.charAt(pos + i)) return false; } return true; } } }