diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/parsing/GdbMiParser.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/parsing/GdbMiParser.java index e4d41063f5..52404c6335 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/parsing/GdbMiParser.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/parsing/GdbMiParser.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import agent.gdb.manager.parsing.GdbParsingUtils.AbstractGdbParser; import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; @@ -30,11 +30,13 @@ import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; /** * A parser for GDB/MI records * + *

* While this is a much more machine-friendly format, it has some interesting idiosyncrasies that * make it annoying even within a machine. This class attempts to impose a nice abstraction of these * records while dealing with nuances particular to certain records, but in general. Examine GDB's * documentation for some example records. * + *

* There seem to be one primitive type and two (and a half?) aggregate types in these records. The * one primitive type is a string. The aggregates are lists and maps, and maybe "field lists" which * behave like multi-valued maps. Maps introduce IDs, which comprise the map keys or field names. @@ -88,7 +90,7 @@ public class GdbMiParser extends AbstractGdbParser { /** * Build the field list * - * @return + * @return the field list */ public GdbMiFieldList build() { return list; @@ -97,41 +99,14 @@ public class GdbMiParser extends AbstractGdbParser { /** * A key-value entry in the field list + * + * @param key the key + * @param value the value */ - public static class Entry { - private final String key; - private final Object value; - - private Entry(String key, Object value) { - this.key = key; - this.value = value; - } - - /** - * Get the key - * - * @return the key - */ - public String getKey() { - return key; - } - - /** - * Get the value - * - * @return the value - */ - public Object getValue() { - return value; - } + public record Entry(String key, Object value) { } - private MultiValuedMap map = new HashSetValuedHashMap() { - @Override - protected HashSet createCollection() { - return new LinkedHashSet<>(); - } - }; + private MultiValuedMap map = new ArrayListValuedHashMap(); private MultiValuedMap unmodifiableMap = MultiMapUtils.unmodifiableMultiValuedMap(map); private final List entryList = new ArrayList<>(); @@ -198,6 +173,7 @@ public class GdbMiParser extends AbstractGdbParser { /** * Assume only a single list is associated with the key, and get that list * + *

* For convenience, the list is cast to a list of elements of a given type. This cast is * unchecked. * @@ -220,10 +196,8 @@ public class GdbMiParser extends AbstractGdbParser { */ public GdbMiFieldList getFieldList(String key) { Object obj = getSingleton(key); - if (obj instanceof List) { - if (((List) obj).isEmpty()) { - return GdbMiFieldList.builder().build(); - } + if (obj instanceof List list && list.isEmpty()) { + return GdbMiFieldList.builder().build(); } return (GdbMiFieldList) obj; } @@ -334,7 +308,7 @@ public class GdbMiParser extends AbstractGdbParser { * * @see #parseObject(CharSequence) * @return the object - * @throws GdbParseError + * @throws GdbParseError if no text matches */ public Object parseObject() throws GdbParseError { switch (peek(true)) { @@ -369,9 +343,11 @@ public class GdbMiParser extends AbstractGdbParser { char ch = buf.get(); if (ch > 0xff) { throw new GdbParseError("byte", "U+" + String.format("%04X", ch)); - } else if (ch == '"') { + } + else if (ch == '"') { break; - } else if (ch != '\\') { + } + else if (ch != '\\') { baos.write(ch); continue; } @@ -495,6 +471,11 @@ public class GdbMiParser extends AbstractGdbParser { result.add(UNNAMED, fieldVal); continue; } + if (c == '"') { + String bareString = parseString(); + result.add(null, bareString); + continue; + } String fieldId = match(FIELD_ID, true); match(EQUALS, true); Object fieldVal = parseObject(); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/parsing/GdbMiParserTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/parsing/GdbMiParserTest.java index 890da36f54..fa09bcd0c2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/parsing/GdbMiParserTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/parsing/GdbMiParserTest.java @@ -18,12 +18,12 @@ package agent.gdb.manager.parsing; import static org.junit.Assert.assertEquals; import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import java.util.regex.Pattern; import org.junit.Test; -import agent.gdb.manager.parsing.GdbMiParser; import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList; import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; @@ -36,41 +36,80 @@ public class GdbMiParserTest { @Test public void testMatch() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("Hello, World!"); + GdbMiParser parser = new GdbMiParser(""" + Hello, World"""); assertEquals("Hello", parser.match(Pattern.compile("\\w+"), true)); assertEquals(",", parser.match(GdbMiParser.COMMA, true)); } @Test public void testParseString() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("\"Hello, World!\\n\""); + GdbMiParser parser = new GdbMiParser(""" + "Hello, World!\\n"\ + """); assertEquals("Hello, World!\n", parser.parseString()); + parser.checkEmpty(false); } @Test public void testParseList() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("[\"Hello\",\"World\"]"); + GdbMiParser parser = new GdbMiParser(""" + ["Hello","World"]"""); assertEquals(Arrays.asList(new String[] { "Hello", "World" }), parser.parseList()); + parser.checkEmpty(false); } @Test public void testParseMap() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("{h=\"Hello\",w=\"World\"}"); + GdbMiParser parser = new GdbMiParser(""" + {h="Hello",w="World"}"""); assertEquals(buildFieldList((exp) -> { exp.add("h", "Hello"); exp.add("w", "World"); }), parser.parseMap()); + parser.checkEmpty(false); } @Test public void testParseStringEscapes() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("\"basic=\\n\\b\\t\\f\\r c=\\e[0m\\a delim=\\\\\\\" octal=\\000\\177\""); - assertEquals("basic=\n\b\t\f\r c=\033[0m\007 delim=\\\" octal=\000\177", parser.parseString()); + GdbMiParser parser = new GdbMiParser(""" + "basic=\\n\\b\\t\\f\\r c=\\e[0m\\a delim=\\\\\\" octal=\\000\\177"\ + """); + assertEquals("basic=\n\b\t\f\r c=\033[0m\007 delim=\\\" octal=\000\177", + parser.parseString()); + parser.checkEmpty(false); } @Test public void testParseStringUTF8() throws GdbParseError { - GdbMiParser parser = new GdbMiParser("\"\\302\\244 \\342\\204\\212 \\343\\201\\251 \\351\\276\\231 \\360\\237\\230\\200\""); + GdbMiParser parser = new GdbMiParser(""" + "\\302\\244 \\342\\204\\212 \\343\\201\\251 \\351\\276\\231 \\360\\237\\230\\200"\ + """); assertEquals("\u00a4 \u210a \u3069 \u9f99 \ud83d\ude00", parser.parseString()); + parser.checkEmpty(false); + } + + @Test + public void testParseBreakpointCommandList() throws GdbParseError { + GdbMiParser parser = new GdbMiParser(""" + BreakpointTable={nr_rows="1",nr_cols="6",hdr=[{width="7",alignment="-1",\ + col_name="number",colhdr="Num"},{width="14",alignment="-1",col_name="type",\ + colhdr="Type"},{width="4",alignment="-1",col_name="disp",colhdr="Disp"},\ + {width="3",alignment="-1",col_name="enabled",colhdr="Enb"},{width="18",\ + alignment="-1",col_name="addr",colhdr="Address"},{width="40",alignment="2",\ + col_name="what",colhdr="What"}],body=[bkpt={number="1",type="breakpoint",\ + disp="keep",enabled="y",addr="0x00007ffff779c96f",at="",\ + thread-groups=["i1"],times="0",script={"echo asdf","echo ghjk","echo asdf"},\ + original-location="*0x7ffff779c96f"}]}"""); + GdbMiFieldList result = parser.parseFields(false); + GdbMiFieldList table = result.getFieldList("BreakpointTable"); + GdbMiFieldList body = table.getFieldList("body"); + List bkpts = List.copyOf(body.get("bkpt")); + assertEquals(1, bkpts.size()); + GdbMiFieldList bkpt0 = (GdbMiFieldList) bkpts.get(0); + GdbMiFieldList script = bkpt0.getFieldList("script"); + List lines = List.copyOf(script.get(null)); + assertEquals(List.of("echo asdf", "echo ghjk", "echo asdf"), lines); + parser.checkEmpty(false); } }