diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/remote-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/remote-gdb.sh index 4ef8f370a9..3048e2a217 100755 --- a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/remote-gdb.sh +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/remote-gdb.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash ## ### -# IP: GHIDRA -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# IP: GHIDRA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. ## #@title remote gdb #@no-image @@ -29,7 +29,7 @@ #@enum TargetType:str remote extended-remote #@env OPT_TARGET_TYPE:TargetType="remote" "Target" "The type of remote target" #@env OPT_HOST:str="localhost" "Host" "The hostname of the target" -#@env OPT_PORT:str="9999" "Port" "The host's listening port" +#@env OPT_PORT:int=9999 "Port" "The host's listening port" #@env OPT_ARCH:str="" "Architecture (optional)" "Target architecture override" #@env OPT_GDB_PATH:file="gdb" "gdb command" "The path to gdb on the local system. Omit the full path to resolve using the system PATH." @@ -60,6 +60,7 @@ fi -ex "show version" \ -ex "python import ghidragdb" \ $archcmd \ + -ex "echo Connecting to $OPT_HOST:$OPT_PORT... " \ -ex "target $OPT_TARGET_TYPE $OPT_HOST:$OPT_PORT" \ -ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \ -ex "ghidra trace start" \ diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/ValStr.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/ValStr.java new file mode 100644 index 0000000000..d9631d3b91 --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/ValStr.java @@ -0,0 +1,59 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.debug.api; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public record ValStr(T val, String str) { + + public interface Decoder { + default ValStr decodeValStr(String string) { + return new ValStr<>(decode(string), string); + } + + T decode(String string); + } + + public static ValStr str(String value) { + return new ValStr<>(value, value); + } + + public static ValStr from(T value) { + return new ValStr<>(value, value == null ? "" : value.toString()); + } + + @SuppressWarnings("unchecked") + public static ValStr cast(Class cls, ValStr value) { + if (cls.isInstance(value.val)) { + return (ValStr) value; + } + return new ValStr<>(cls.cast(value.val), value.str); + } + + public static Map> fromPlainMap(Map map) { + return map.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> ValStr.from(e.getValue()))); + } + + public static Map toPlainMap(Map> map) { + return map.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().val())); + } +} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerProgramLaunchOffer.java index 7b43e4e5a4..9e72519c55 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerProgramLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,6 +24,7 @@ import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.TargetLauncher; import ghidra.dbg.target.TargetObject; +import ghidra.debug.api.ValStr; import ghidra.util.task.TaskMonitor; /** @@ -117,8 +118,8 @@ public interface DebuggerProgramLaunchOffer { * @param relPrompt describes the timing of this callback relative to prompting the user * @return the adjusted arguments */ - default Map configureLauncher(TargetLauncher launcher, - Map arguments, RelPrompt relPrompt) { + default Map> configureLauncher(TargetLauncher launcher, + Map> arguments, RelPrompt relPrompt) { return arguments; } } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/LaunchParameter.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/LaunchParameter.java new file mode 100644 index 0000000000..d2cc2de391 --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/LaunchParameter.java @@ -0,0 +1,106 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.debug.api.tracermi; + +import java.util.*; + +import ghidra.debug.api.ValStr; + +public record LaunchParameter(Class type, String name, String display, String description, + boolean required, List choices, ValStr defaultValue, ValStr.Decoder decoder) { + + public static LaunchParameter create(Class type, String name, String display, + String description, boolean required, ValStr defaultValue, + ValStr.Decoder decoder) { + return new LaunchParameter<>(type, name, display, description, required, List.of(), + defaultValue, decoder); + } + + public static LaunchParameter choices(Class type, String name, String display, + String description, Collection choices, ValStr defaultValue) { + return new LaunchParameter<>(type, name, display, description, false, + List.copyOf(new LinkedHashSet<>(choices)), defaultValue, str -> { + for (T t : choices) { + if (t.toString().equals(str)) { + return t; + } + } + return null; + }); + } + + public static Map> mapOf(Collection> parameters) { + Map> result = new LinkedHashMap<>(); + for (LaunchParameter param : parameters) { + LaunchParameter exists = result.put(param.name(), param); + if (exists != null) { + throw new IllegalArgumentException( + "Duplicate names in parameter map: first=%s, second=%s".formatted(exists, + param)); + } + } + return Collections.unmodifiableMap(result); + } + + public static Map> validateArguments( + Map> parameters, Map> arguments) { + if (!parameters.keySet().containsAll(arguments.keySet())) { + Set extraneous = new TreeSet<>(arguments.keySet()); + extraneous.removeAll(parameters.keySet()); + throw new IllegalArgumentException("Extraneous parameters: " + extraneous); + } + + Map typeErrors = null; + for (Map.Entry> ent : arguments.entrySet()) { + String name = ent.getKey(); + ValStr val = ent.getValue(); + LaunchParameter param = parameters.get(name); + if (val.val() != null && !param.type.isAssignableFrom(val.val().getClass())) { + if (typeErrors == null) { + typeErrors = new LinkedHashMap<>(); + } + typeErrors.put(name, "val '%s' is not a %s".formatted(val.val(), param.type())); + } + } + if (typeErrors != null) { + throw new IllegalArgumentException("Type errors: " + typeErrors); + } + return arguments; + } + + public static Map> mapOf(LaunchParameter... parameters) { + return mapOf(Arrays.asList(parameters)); + } + + public ValStr decode(String string) { + return decoder.decodeValStr(string); + } + + public ValStr get(Map> arguments) { + if (arguments.containsKey(name)) { + return ValStr.cast(type, arguments.get(name)); + } + if (required) { + throw new IllegalArgumentException( + "Missing required parameter '%s' (%s)".formatted(display, name)); + } + return defaultValue; + } + + public void set(Map> arguments, ValStr value) { + arguments.put(name, value); + } +} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java index 85c16bbeb2..6351a965c7 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ import java.util.Map; import javax.swing.Icon; -import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.debug.api.ValStr; import ghidra.program.model.listing.Program; import ghidra.trace.model.Trace; import ghidra.util.HelpLocation; @@ -150,8 +150,8 @@ public interface TraceRmiLaunchOffer { * @param relPrompt describes the timing of this callback relative to prompting the user * @return the adjusted arguments */ - default Map configureLauncher(TraceRmiLaunchOffer offer, - Map arguments, RelPrompt relPrompt) { + default Map> configureLauncher(TraceRmiLaunchOffer offer, + Map> arguments, RelPrompt relPrompt) { return arguments; } } @@ -293,7 +293,7 @@ public interface TraceRmiLaunchOffer { * * @return the parameters */ - Map> getParameters(); + Map> getParameters(); /** * Check if this offer requires an open program diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java index 118732a5db..33a2b03a63 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,6 +28,7 @@ import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.util.PathUtils; +import ghidra.debug.api.ValStr; import ghidra.debug.api.model.DebuggerProgramLaunchOffer; import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*; import ghidra.debug.api.model.TraceRecorder; @@ -578,10 +579,10 @@ public interface FlatDebuggerRecorderAPI extends FlatDebuggerAPI { try { return waitOn(offer.launchProgram(monitor, PromptMode.NEVER, new LaunchConfigurator() { @Override - public Map configureLauncher(TargetLauncher launcher, - Map arguments, RelPrompt relPrompt) { - Map adjusted = new HashMap<>(arguments); - adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine); + public Map> configureLauncher(TargetLauncher launcher, + Map> arguments, RelPrompt relPrompt) { + Map> adjusted = new HashMap<>(arguments); + adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, ValStr.str(commandLine)); return adjusted; } })); diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java index e4f71f1ad3..479b96d5d4 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,8 +16,10 @@ package ghidra.debug.flatapi; import java.util.*; +import java.util.Map.Entry; import ghidra.app.services.TraceRmiLauncherService; +import ghidra.debug.api.ValStr; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; import ghidra.program.model.listing.Program; @@ -116,13 +118,15 @@ public interface FlatDebuggerRmiAPI extends FlatDebuggerAPI { TaskMonitor monitor) { return offer.launchProgram(monitor, new LaunchConfigurator() { @Override - public Map configureLauncher(TraceRmiLaunchOffer offer, - Map arguments, RelPrompt relPrompt) { + public Map> configureLauncher(TraceRmiLaunchOffer offer, + Map> arguments, RelPrompt relPrompt) { if (arguments.isEmpty()) { return arguments; } - Map args = new HashMap<>(arguments); - args.putAll(overrideArgs); + Map> args = new HashMap<>(arguments); + for (Entry ent : overrideArgs.entrySet()) { + args.put(ent.getKey(), ValStr.from(ent.getValue())); + } return args; } }); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java index 3c582a5dfc..cb049e0540 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,397 +15,128 @@ */ package ghidra.app.plugin.core.debug.gui.tracermi; -import java.awt.BorderLayout; import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Graphics; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.beans.PropertyEditor; -import java.beans.PropertyEditorManager; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.beans.*; +import java.util.*; -import javax.swing.BorderFactory; import javax.swing.Icon; -import javax.swing.JButton; import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.border.EmptyBorder; -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.text.StringEscapeUtils; -import org.jdom.Element; - -import docking.DialogComponentProvider; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; -import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils; +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget.Missing; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.SchemaContext; +import ghidra.debug.api.ValStr; import ghidra.debug.api.tracermi.RemoteParameter; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.AutoConfigState.ConfigStateField; import ghidra.framework.plugintool.PluginTool; -import ghidra.util.Msg; -import ghidra.util.layout.PairLayout; +import ghidra.trace.model.target.TraceObject; -public class RemoteMethodInvocationDialog extends DialogComponentProvider - implements PropertyChangeListener { - private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments"; +public class RemoteMethodInvocationDialog extends AbstractDebuggerParameterDialog { - static class ChoicesPropertyEditor implements PropertyEditor { - private final List choices; - private final String[] tags; - - private final List listeners = new ArrayList<>(); - - private Object value; - - public ChoicesPropertyEditor(Set choices) { - this.choices = List.copyOf(choices); - this.tags = choices.stream().map(Objects::toString).toArray(String[]::new); - } + /** + * TODO: Make this a proper editor which can browse and select objects of a required schema. + */ + public static class TraceObjectEditor extends PropertyEditorSupport { + private final JLabel unmodifiableField = new JLabel(); @Override public void setValue(Object value) { - if (Objects.equals(value, this.value)) { + super.setValue(value); + if (value == null) { + unmodifiableField.setText(""); return; } - if (!choices.contains(value)) { - throw new IllegalArgumentException("Unsupported value: " + value); + if (!(value instanceof TraceObject obj)) { + throw new IllegalArgumentException(); } - Object oldValue; - List listeners; - synchronized (this.listeners) { - oldValue = this.value; - this.value = value; - if (this.listeners.isEmpty()) { - return; - } - listeners = List.copyOf(this.listeners); - } - PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value); - for (PropertyChangeListener l : listeners) { - l.propertyChange(evt); - } - } - - @Override - public Object getValue() { - return value; - } - - @Override - public boolean isPaintable() { - return false; - } - - @Override - public void paintValue(Graphics gfx, Rectangle box) { - // Not paintable - } - - @Override - public String getJavaInitializationString() { - if (value == null) { - return "null"; - } - if (value instanceof String str) { - return "\"" + StringEscapeUtils.escapeJava(str) + "\""; - } - return Objects.toString(value); - } - - @Override - public String getAsText() { - return Objects.toString(value); - } - - @Override - public void setAsText(String text) throws IllegalArgumentException { - int index = ArrayUtils.indexOf(tags, text); - if (index < 0) { - throw new IllegalArgumentException("Unsupported value: " + text); - } - setValue(choices.get(index)); - } - - @Override - public String[] getTags() { - return tags.clone(); - } - - @Override - public Component getCustomEditor() { - return null; + unmodifiableField.setText(obj.getCanonicalPath().toString()); } @Override public boolean supportsCustomEditor() { - return false; + return true; } @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - synchronized (listeners) { - listeners.add(listener); - } - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - synchronized (listeners) { - listeners.remove(listener); - } + public Component getCustomEditor() { + return unmodifiableField; } } - record NameTypePair(String name, Class type) { - public static NameTypePair fromParameter(SchemaContext ctx, RemoteParameter parameter) { - return new NameTypePair(parameter.name(), ctx.getSchema(parameter.type()).getType()); - } - - public static NameTypePair fromString(String name) throws ClassNotFoundException { - String[] parts = name.split(",", 2); - if (parts.length != 2) { - // This appears to be a bad assumption - empty fields results in solitary labels - return new NameTypePair(parts[0], String.class); - //throw new IllegalArgumentException("Could not parse name,type"); - } - return new NameTypePair(parts[0], Class.forName(parts[1])); - } + static { + PropertyEditorManager.registerEditor(TraceObject.class, TraceObjectEditor.class); } - private final BidiMap paramEditors = - new DualLinkedHashBidiMap<>(); + private final SchemaContext ctx; - private JPanel panel; - private JLabel descriptionLabel; - private JPanel pairPanel; - private PairLayout layout; - - protected JButton invokeButton; - protected JButton resetButton; - - private final PluginTool tool; - private SchemaContext ctx; - private Map parameters; - private Map defaults; - - // TODO: Not sure this is the best keying, but I think it works. - private Map memorized = new HashMap<>(); - private Map arguments; - - public RemoteMethodInvocationDialog(PluginTool tool, String title, String buttonText, - Icon buttonIcon) { - super(title, true, true, true, false); - this.tool = tool; - - populateComponents(buttonText, buttonIcon); - setRememberSize(false); - } - - protected Object computeMemorizedValue(RemoteParameter parameter) { - return memorized.computeIfAbsent(NameTypePair.fromParameter(ctx, parameter), - ntp -> parameter.getDefaultValue()); - } - - public Map promptArguments(SchemaContext ctx, - Map parameterMap, Map defaults) { - setParameters(ctx, parameterMap); - setDefaults(defaults); - tool.showDialog(this); - - return getArguments(); - } - - public void setParameters(SchemaContext ctx, Map parameterMap) { + public RemoteMethodInvocationDialog(PluginTool tool, SchemaContext ctx, String title, + String buttonText, Icon buttonIcon) { + super(tool, title, buttonText, buttonIcon); this.ctx = ctx; - this.parameters = parameterMap; - populateOptions(); - } - - public void setDefaults(Map defaults) { - this.defaults = defaults; - } - - private void populateComponents(String buttonText, Icon buttonIcon) { - panel = new JPanel(new BorderLayout()); - panel.setBorder(new EmptyBorder(10, 10, 10, 10)); - - layout = new PairLayout(5, 5); - pairPanel = new JPanel(layout); - - JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER)); - JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, - JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - //scrolling.setPreferredSize(new Dimension(100, 130)); - panel.add(scrolling, BorderLayout.CENTER); - centering.add(pairPanel); - - descriptionLabel = new JLabel(); - descriptionLabel.setMaximumSize(new Dimension(300, 100)); - panel.add(descriptionLabel, BorderLayout.NORTH); - - addWorkPanel(panel); - - invokeButton = new JButton(buttonText, buttonIcon); - addButton(invokeButton); - resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH); - addButton(resetButton); - addCancelButton(); - - invokeButton.addActionListener(this::invoke); - resetButton.addActionListener(this::reset); } @Override - protected void cancelCallback() { - this.arguments = null; - close(); + protected String parameterName(RemoteParameter parameter) { + return parameter.name(); } - protected void invoke(ActionEvent evt) { - this.arguments = collectArguments(); - close(); - } - - private void reset(ActionEvent evt) { - this.arguments = new HashMap<>(); - for (RemoteParameter param : parameters.values()) { - if (defaults.containsKey(param.name())) { - arguments.put(param.name(), defaults.get(param.name())); - } - else { - arguments.put(param.name(), param.getDefaultValue()); - } + @Override + protected Class parameterType(RemoteParameter parameter) { + Class type = ctx.getSchema(parameter.type()).getType(); + if (TargetObject.class.isAssignableFrom(type)) { + return TraceObject.class; } - populateValues(); + return type; } - protected PropertyEditor createEditor(RemoteParameter param) { - Class type = ctx.getSchema(param.type()).getType(); - PropertyEditor editor = PropertyEditorManager.findEditor(type); - if (editor != null) { - return editor; - } - Msg.warn(this, "No editor for " + type + "? Trying String instead"); - return PropertyEditorManager.findEditor(String.class); + @Override + protected String parameterLabel(RemoteParameter parameter) { + return "".equals(parameter.display()) ? parameter.name() : parameter.display(); } - void populateOptions() { - pairPanel.removeAll(); - paramEditors.clear(); - for (RemoteParameter param : parameters.values()) { - String text = param.display().equals("") ? param.name() : param.display(); - JLabel label = new JLabel(text); - label.setToolTipText(param.description()); - pairPanel.add(label); - - PropertyEditor editor = createEditor(param); - Object val = computeMemorizedValue(param); - if (val == null || val.equals(TraceRmiTarget.Missing.MISSING)) { - editor.setValue(""); - } else { - editor.setValue(val); - } - editor.addPropertyChangeListener(this); - pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); - paramEditors.put(param, editor); - } + @Override + protected String parameterToolTip(RemoteParameter parameter) { + return parameter.description(); } - void populateValues() { - for (Map.Entry ent : arguments.entrySet()) { - RemoteParameter param = parameters.get(ent.getKey()); - if (param == null) { - Msg.warn(this, "No parameter for argument: " + ent); - continue; - } - PropertyEditor editor = paramEditors.get(param); - editor.setValue(ent.getValue()); - } + @Override + protected ValStr parameterDefault(RemoteParameter parameter) { + return ValStr.from(parameter.getDefaultValue()); } - protected Map collectArguments() { - Map map = new LinkedHashMap<>(); - for (RemoteParameter param : paramEditors.keySet()) { - Object val = memorized.get(NameTypePair.fromParameter(ctx, param)); - if (val != null) { - map.put(param.name(), val); - } - } - return map; + @Override + protected Collection parameterChoices(RemoteParameter parameter) { + return Set.of(); } - public Map getArguments() { + @Override + protected Map> validateArguments(Map parameters, + Map> arguments) { return arguments; } - public void setMemorizedArgument(String name, Class type, T value) { - if (value == null) { - return; - } - memorized.put(new NameTypePair(name, type), value); - } - - public T getMemorizedArgument(String name, Class type) { - return type.cast(memorized.get(new NameTypePair(name, type))); + @Override + protected void parameterSaveValue(RemoteParameter parameter, SaveState state, String key, + ValStr value) { + ConfigStateField.putState(state, parameterType(parameter).asSubclass(Object.class), key, + value.val()); } @Override - public void propertyChange(PropertyChangeEvent evt) { - PropertyEditor editor = (PropertyEditor) evt.getSource(); - RemoteParameter param = paramEditors.getKey(editor); - memorized.put(NameTypePair.fromParameter(ctx, param), editor.getValue()); + protected ValStr parameterLoadValue(RemoteParameter parameter, SaveState state, String key) { + return ValStr.from( + ConfigStateField.getState(state, parameterType(parameter), key)); } - public void writeConfigState(SaveState saveState) { - SaveState subState = new SaveState(); - for (Map.Entry ent : memorized.entrySet()) { - NameTypePair ntp = ent.getKey(); - ConfigStateField.putState(subState, ntp.type().asSubclass(Object.class), ntp.name(), - ent.getValue()); - } - saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml()); - } - - public void readConfigState(SaveState saveState) { - Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS); - if (element == null) { - return; - } - SaveState subState = new SaveState(element); - for (String name : subState.getNames()) { - try { - NameTypePair ntp = NameTypePair.fromString(name); - memorized.put(ntp, ConfigStateField.getState(subState, ntp.type(), ntp.name())); - } - catch (Exception e) { - Msg.error(this, "Error restoring memorized parameter " + name, e); - } - } - } - - public void setDescription(String htmlDescription) { - if (htmlDescription == null) { - descriptionLabel.setBorder(BorderFactory.createEmptyBorder()); - descriptionLabel.setText(""); - } - else { - descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - descriptionLabel.setText(htmlDescription); - } + @Override + protected void setEditorValue(PropertyEditor editor, RemoteParameter param, ValStr val) { + ValStr v = switch (val.val()) { + case Missing __ -> new ValStr<>(null, ""); + case TraceObject obj -> new ValStr<>(obj, obj.getCanonicalPath().toString()); + default -> val; + }; + super.setEditorValue(editor, param, v); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectDialog.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectDialog.java new file mode 100644 index 0000000000..99960efc18 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectDialog.java @@ -0,0 +1,46 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.connection; + +import java.util.Map; + +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLaunchDialog; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.LaunchParameter; +import ghidra.framework.plugintool.PluginTool; + +public class TraceRmiConnectDialog extends TraceRmiLaunchDialog { + + static final LaunchParameter PARAM_ADDRESS = + LaunchParameter.create(String.class, "address", + "Host/Address", "Address or hostname for interface(s) to listen on", + true, ValStr.str("localhost"), str -> str); + static final LaunchParameter PARAM_PORT = + LaunchParameter.create(Integer.class, "port", + "Port", "TCP port number, 0 for ephemeral", + true, ValStr.from(0), Integer::decode); + private static final Map> PARAMETERS = + LaunchParameter.mapOf(PARAM_ADDRESS, PARAM_PORT); + + public TraceRmiConnectDialog(PluginTool tool, String title, String buttonText) { + super(tool, title, buttonText, DebuggerResources.ICON_CONNECTION); + } + + public Map> promptArguments() { + return promptArguments(PARAMETERS, Map.of(), Map.of()); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java index 676b6e5a1a..9ab1fdedaa 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -34,11 +34,9 @@ import docking.action.builder.ActionBuilder; import docking.widgets.tree.*; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*; import ghidra.app.services.*; -import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.debug.api.ValStr; import ghidra.debug.api.control.ControlMode; import ghidra.debug.api.target.Target; import ghidra.debug.api.tracemgr.DebuggerCoordinates; @@ -62,16 +60,6 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter private static final String GROUP_CONNECT = "1. Connect"; private static final String GROUP_MAINTENANCE = "3. Maintenance"; - private static final ParameterDescription PARAM_ADDRESS = - ParameterDescription.create(String.class, "address", true, "localhost", - "Host/Address", "Address or hostname for interface(s) to listen on"); - private static final ParameterDescription PARAM_PORT = - ParameterDescription.create(Integer.class, "port", true, 0, - "Port", "TCP port number, 0 for ephemeral"); - private static final TargetParameterMap PARAMETERS = TargetParameterMap.ofEntries( - Map.entry(PARAM_ADDRESS.name, PARAM_ADDRESS), - Map.entry(PARAM_PORT.name, PARAM_PORT)); - interface StartServerAction { String NAME = "Start Server"; String DESCRIPTION = "Start a TCP server for incoming connections (indefinitely)"; @@ -344,25 +332,24 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter return traceRmiService != null && !traceRmiService.isServerStarted(); } - private InetSocketAddress promptSocketAddress(String title, String okText) { - DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool, - title, okText, DebuggerResources.ICON_CONNECTION); - Map arguments; - do { - dialog.forgetMemorizedArguments(); - arguments = dialog.promptArguments(PARAMETERS); - } - while (dialog.isResetRequested()); + private InetSocketAddress promptSocketAddress(String title, String okText, + HelpLocation helpLocation) { + TraceRmiConnectDialog dialog = new TraceRmiConnectDialog(tool, title, okText); + dialog.setHelpLocation(helpLocation); + Map> arguments = dialog.promptArguments(); + if (arguments == null) { + // Cancelled return null; } - String address = PARAM_ADDRESS.get(arguments); - int port = PARAM_PORT.get(arguments); + String address = TraceRmiConnectDialog.PARAM_ADDRESS.get(arguments).val(); + int port = TraceRmiConnectDialog.PARAM_PORT.get(arguments).val(); return new InetSocketAddress(address, port); } private void doActionStartServerActivated(ActionContext __) { - InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start"); + InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start", + actionStartServer.getHelpLocation()); if (sockaddr == null) { return; } @@ -395,7 +382,8 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter } private void doActionConnectAcceptActivated(ActionContext __) { - InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen"); + InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen", + actionConnectAccept.getHelpLocation()); if (sockaddr == null) { return; } @@ -420,7 +408,8 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter } private void doActionConnectOutboundActivated(ActionContext __) { - InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect"); + InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect", + actionConnectOutbound.getHelpLocation()); if (sockaddr == null) { return; } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java index effa0aac33..21b1567c2f 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,7 +23,8 @@ import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.TtyCondition; -import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.LaunchParameter; import ghidra.debug.api.tracermi.TerminalSession; import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; @@ -84,7 +85,7 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi } @Override - public Map> getParameters() { + public Map> getParameters() { return attrs.parameters(); } @@ -93,12 +94,15 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi return attrs.timeoutMillis(); } - protected abstract void prepareSubprocess(List commandLine, Map env, - Map args, SocketAddress address); + protected void prepareSubprocess(List commandLine, Map env, + Map> args, SocketAddress address) { + ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args, + address); + } @Override protected void launchBackEnd(TaskMonitor monitor, Map sessions, - Map args, SocketAddress address) throws Exception { + Map> args, SocketAddress address) throws Exception { List commandLine = new ArrayList<>(); Map env = new HashMap<>(System.getenv()); prepareSubprocess(commandLine, env, args, address); @@ -112,7 +116,7 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi } NullPtyTerminalSession ns = nullPtyTerminal(); env.put(ent.getKey(), ns.name()); - sessions.put(ns.name(), ns); + sessions.put(ent.getKey(), ns); } sessions.put("Shell", diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index a8e113c610..ba882eca4a 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -28,7 +28,7 @@ import java.util.concurrent.*; import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; +import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.LaunchFailureDialog.ErrPromptResponse; import ghidra.app.plugin.core.debug.service.tracermi.DefaultTraceRmiAcceptor; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler; @@ -36,8 +36,8 @@ import ghidra.app.plugin.core.terminal.TerminalListener; import ghidra.app.services.*; import ghidra.app.services.DebuggerTraceManagerService.ActivationCause; import ghidra.async.AsyncUtils; -import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.util.ShellUtils; +import ghidra.debug.api.ValStr; import ghidra.debug.api.action.AutoMapSpec; import ghidra.debug.api.modules.DebuggerMissingProgramActionContext; import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener; @@ -212,14 +212,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer return mappingService.getOpenMappedLocation(trace, probe, snap) != null; } - protected SaveState saveLauncherArgsToState(Map args, - Map> params) { + protected SaveState saveLauncherArgsToState(Map> args, + Map> params) { SaveState state = new SaveState(); - for (ParameterDescription param : params.values()) { - Object val = args.get(param.name); + for (LaunchParameter param : params.values()) { + ValStr val = args.get(param.name()); if (val != null) { - ConfigStateField.putState(state, param.type.asSubclass(Object.class), - "param_" + param.name, val); + state.putString("param_" + param.name(), val.str()); } } return state; @@ -233,56 +232,56 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer plugin.writeProgramLaunchConfig(program, getConfigName(), state); } - protected void saveLauncherArgs(Map args, - Map> params) { + protected void saveLauncherArgs(Map> args, + Map> params) { saveState(saveLauncherArgsToState(args, params)); } interface ImageParamSetter { @SuppressWarnings("unchecked") - static ImageParamSetter get(ParameterDescription param) { - if (param.type == String.class) { - return new StringImageParamSetter((ParameterDescription) param); + static ImageParamSetter get(LaunchParameter param) { + if (param.type() == String.class) { + return new StringImageParamSetter((LaunchParameter) param); } - if (param.type == PathIsFile.class) { - return new FileImageParamSetter((ParameterDescription) param); + if (param.type() == PathIsFile.class) { + return new FileImageParamSetter((LaunchParameter) param); } Msg.warn(ImageParamSetter.class, - "'Image' parameter has unsupported type: " + param.type); + "'Image' parameter has unsupported type: " + param.type()); return null; } - void setImage(Map map, Program program); + void setImage(Map> map, Program program); } static class StringImageParamSetter implements ImageParamSetter { - private final ParameterDescription param; + private final LaunchParameter param; - public StringImageParamSetter(ParameterDescription param) { + public StringImageParamSetter(LaunchParameter param) { this.param = param; } @Override - public void setImage(Map map, Program program) { + public void setImage(Map> map, Program program) { // str-type Image is a hint that the launcher is remote String value = TraceRmiLauncherServicePlugin.getProgramPath(program, false); - param.set(map, value); + param.set(map, ValStr.str(value)); } } static class FileImageParamSetter implements ImageParamSetter { - private final ParameterDescription param; + private final LaunchParameter param; - public FileImageParamSetter(ParameterDescription param) { + public FileImageParamSetter(LaunchParameter param) { this.param = param; } @Override - public void setImage(Map map, Program program) { + public void setImage(Map> map, Program program) { // file-type Image is a hint that the launcher is local String str = TraceRmiLauncherServicePlugin.getProgramPath(program, true); PathIsFile value = str == null ? null : new PathIsFile(Paths.get(str)); - param.set(map, value); + param.set(map, new ValStr<>(value, str)); } } @@ -297,33 +296,34 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer * @return the default arguments */ @SuppressWarnings("unchecked") - protected Map generateDefaultLauncherArgs( - Map> params) { - Map map = new LinkedHashMap(); + protected Map> generateDefaultLauncherArgs( + Map> params) { + Map> map = new LinkedHashMap<>(); ImageParamSetter imageSetter = null; - for (Entry> entry : params.entrySet()) { - ParameterDescription param = entry.getValue(); - map.put(entry.getKey(), param.defaultValue); - if (PARAM_DISPLAY_IMAGE.equals(param.display)) { + for (Entry> entry : params.entrySet()) { + LaunchParameter param = entry.getValue(); + map.put(entry.getKey(), ValStr.cast(Object.class, param.defaultValue())); + if (PARAM_DISPLAY_IMAGE.equals(param.display())) { imageSetter = ImageParamSetter.get(param); // May still be null if type is not supported } - else if (param.name.startsWith(PREFIX_PARAM_EXTTOOL)) { - String tool = param.name.substring(PREFIX_PARAM_EXTTOOL.length()); + else if (param.name().startsWith(PREFIX_PARAM_EXTTOOL)) { + String tool = param.name().substring(PREFIX_PARAM_EXTTOOL.length()); List names = program.getLanguage().getLanguageDescription().getExternalNames(tool); if (names != null && !names.isEmpty()) { - if (param.type == String.class) { - var paramStr = (ParameterDescription) param; - paramStr.set(map, names.get(0)); + String toolName = names.get(0); + if (param.type() == String.class) { + var paramStr = (LaunchParameter) param; + paramStr.set(map, ValStr.str(toolName)); } - else if (param.type == PathIsFile.class) { - var paramPIF = (ParameterDescription) param; - paramPIF.set(map, PathIsFile.fromString(names.get(0))); + else if (param.type() == PathIsFile.class) { + var paramPIF = (LaunchParameter) param; + paramPIF.set(map, new ValStr<>(PathIsFile.fromString(toolName), toolName)); } - else if (param.type == PathIsDir.class) { - var paramPID = (ParameterDescription) param; - paramPID.set(map, PathIsDir.fromString(names.get(0))); + else if (param.type() == PathIsDir.class) { + var paramPID = (LaunchParameter) param; + paramPID.set(map, new ValStr<>(PathIsDir.fromString(toolName), toolName)); } } } @@ -337,50 +337,33 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer /** * Prompt the user for arguments, showing those last used or defaults * - * @param lastExc - * - * @param params the parameters of the model's launcher + * @param configurator a thing to generate/modify the (default) arguments * @param lastExc if re-prompting, an error to display * @return the arguments given by the user, or null if cancelled */ - protected Map promptLauncherArgs(LaunchConfigurator configurator, + protected Map> promptLauncherArgs(LaunchConfigurator configurator, Throwable lastExc) { - Map> params = getParameters(); - DebuggerMethodInvocationDialog dialog = - new DebuggerMethodInvocationDialog(tool, getTitle(), "Launch", getIcon()); + Map> params = getParameters(); + TraceRmiLaunchDialog dialog = + new TraceRmiLaunchDialog(tool, getTitle(), "Launch", getIcon()); dialog.setDescription(getDescription()); dialog.setHelpLocation(getHelpLocation()); + if (lastExc != null) { + dialog.setStatusText(lastExc.toString(), MessageType.ERROR); + } + else { + dialog.setStatusText(""); + } + // NB. Do not invoke read/writeConfigState - Map args; - boolean reset = false; - do { - args = - configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE); - for (ParameterDescription param : params.values()) { - Object val = args.get(param.name); - if (val != null) { - dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class), - val); - } - } - if (lastExc != null) { - dialog.setStatusText(lastExc.toString(), MessageType.ERROR); - } - else { - dialog.setStatusText(""); - } - args = dialog.promptArguments(params); - if (args == null) { - // Cancelled - return null; - } - reset = dialog.isResetRequested(); - if (reset) { - args = generateDefaultLauncherArgs(params); - } + + Map> defaultArgs = generateDefaultLauncherArgs(params); + Map> lastArgs = + configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE); + Map> args = dialog.promptArguments(params, lastArgs, defaultArgs); + if (args != null) { saveLauncherArgs(args, params); } - while (reset); return args; } @@ -398,31 +381,40 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer * @param forPrompt true if the user will be confirming the arguments * @return the loaded arguments, or defaults */ - protected Map loadLastLauncherArgs(boolean forPrompt) { - Map> params = getParameters(); - Map args = loadLauncherArgsFromState(loadState(forPrompt), params); + protected Map> loadLastLauncherArgs(boolean forPrompt) { + Map> params = getParameters(); + Map> args = loadLauncherArgsFromState(loadState(forPrompt), params); saveLauncherArgs(args, params); return args; } - protected Map loadLauncherArgsFromState(SaveState state, - Map> params) { - Map defaultArgs = generateDefaultLauncherArgs(params); + protected Map> loadLauncherArgsFromState(SaveState state, + Map> params) { + Map> defaultArgs = generateDefaultLauncherArgs(params); if (state == null) { return defaultArgs; } - List names = List.of(state.getNames()); - Map args = new LinkedHashMap<>(); - for (ParameterDescription param : params.values()) { - String key = "param_" + param.name; - Object configState = - names.contains(key) ? ConfigStateField.getState(state, param.type, key) : null; - if (configState != null) { - args.put(param.name, configState); + Map> args = new LinkedHashMap<>(); + Set names = Set.of(state.getNames()); + for (LaunchParameter param : params.values()) { + String key = "param_" + param.name(); + if (!names.contains(key)) { + args.put(param.name(), defaultArgs.get(param.name())); + continue; } - else { - args.put(param.name, defaultArgs.get(param.name)); + String str = state.getString(key, null); + if (str != null) { + args.put(param.name(), param.decode(str)); + continue; } + // Perhaps wrong type; was saved in older version. + Object fallback = ConfigStateField.getState(state, param.type(), param.name()); + if (fallback != null) { + args.put(param.name(), ValStr.from(fallback)); + continue; + } + Msg.warn(this, "Could not load saved launcher arg '%s' (%s)".formatted(param.name(), + param.display())); } return args; } @@ -435,7 +427,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } /** - * Obtain the launcher args + * Obtain the launcher arguments * *

* This should either call {@link #promptLauncherArgs(LaunchConfigurator, Throwable)} or @@ -447,7 +439,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer * @param lastExc if retrying, the last exception to display as an error message * @return the chosen arguments, or null if the user cancels at the prompt */ - public Map getLauncherArgs(boolean prompt, LaunchConfigurator configurator, + public Map> getLauncherArgs(boolean prompt, LaunchConfigurator configurator, Throwable lastExc) { return prompt ? configurator.configureLauncher(this, promptLauncherArgs(configurator, lastExc), @@ -543,8 +535,8 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } protected abstract void launchBackEnd(TaskMonitor monitor, - Map sessions, Map args, SocketAddress address) - throws Exception; + Map sessions, Map> args, + SocketAddress address) throws Exception; static class NoStaticMappingException extends Exception { public NoStaticMappingException(String message) { @@ -557,9 +549,18 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } } - protected void initializeMonitor(TaskMonitor monitor) { + protected AutoMapSpec getAutoMapSpec() { DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class); - AutoMapSpec spec = auto.getAutoMapSpec(); + return auto == null ? ByModuleAutoMapSpec.instance() : auto.getAutoMapSpec(); + } + + protected AutoMapSpec getAutoMapSpec(Trace trace) { + DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class); + return auto == null ? ByModuleAutoMapSpec.instance() : auto.getAutoMapSpec(trace); + } + + protected void initializeMonitor(TaskMonitor monitor) { + AutoMapSpec spec = getAutoMapSpec(); if (requiresImage() && spec.hasTask()) { monitor.setMaximum(6); } @@ -574,8 +575,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer if (!requiresImage()) { return; } - DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class); - AutoMapSpec spec = auto.getAutoMapSpec(trace); + AutoMapSpec spec = getAutoMapSpec(trace); if (!spec.hasTask()) { return; } @@ -625,7 +625,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer while (true) { try { monitor.setMessage("Gathering arguments"); - Map args = getLauncherArgs(prompt, configurator, lastExc); + Map> args = getLauncherArgs(prompt, configurator, lastExc); if (args == null) { if (lastExc == null) { lastExc = new CancelledException(); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java index b883ae3d01..ee2ecd2ff7 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,18 +22,31 @@ import java.util.List; import java.util.Map; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.debug.api.ValStr; import ghidra.program.model.listing.Program; /** * A launcher implemented by a simple DOS/Windows batch file. * *

- * The script must start with an attributes header in a comment block. + * The script must start with an attributes header in a comment block. See + * {@link ScriptAttributesParser}. */ public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer { public static final String REM = "::"; public static final int REM_LEN = REM.length(); + /** + * Create a launch offer from the given batch file. + * + * @param plugin the launcher service plugin + * @param program the current program, usually the target image. In general, this should be used + * for at least two purposes. 1) To populate the default command line. 2) To ensure + * the target image is mapped in the resulting target trace. + * @param script the batch file that implements this offer + * @return the offer + * @throws FileNotFoundException if the batch file does not exist + */ public static BatchScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin, Program program, File script) throws FileNotFoundException { ScriptAttributesParser parser = new ScriptAttributesParser() { @@ -60,11 +73,4 @@ public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunch File script, String configName, ScriptAttributes attrs) { super(plugin, program, script, configName, attrs); } - - @Override - protected void prepareSubprocess(List commandLine, Map env, - Map args, SocketAddress address) { - ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args, - address); - } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java index 2722740ec3..90bb8110fd 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,18 +27,23 @@ import javax.swing.Icon; import generic.theme.GIcon; import generic.theme.Gui; -import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.util.ShellUtils; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.LaunchParameter; import ghidra.framework.Application; import ghidra.framework.plugintool.AutoConfigState.PathIsDir; import ghidra.framework.plugintool.AutoConfigState.PathIsFile; -import ghidra.util.HelpLocation; -import ghidra.util.Msg; +import ghidra.util.*; /** * A parser for reading attributes from a script header */ public abstract class ScriptAttributesParser { + public static final String ENV_GHIDRA_HOME = "GHIDRA_HOME"; + public static final String ENV_GHIDRA_TRACE_RMI_ADDR = "GHIDRA_TRACE_RMI_ADDR"; + public static final String ENV_GHIDRA_TRACE_RMI_HOST = "GHIDRA_TRACE_RMI_HOST"; + public static final String ENV_GHIDRA_TRACE_RMI_PORT = "GHIDRA_TRACE_RMI_PORT"; + public static final String AT_TITLE = "@title"; public static final String AT_DESC = "@desc"; public static final String AT_MENU_PATH = "@menu-path"; @@ -69,10 +74,29 @@ public abstract class ScriptAttributesParser { public static final String MSGPAT_INVALID_ARGS_SYNTAX = "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\""; public static final String MSGPAT_INVALID_TTY_SYNTAX = - "%s: Invalid %s syntax. Use TTY_TARGET [if env:OPT_EXTRA_TTY]"; + "%s: Invalid %s syntax. Use TTY_TARGET [if env:OPT [== VAL]]"; + public static final String MSGPAT_INVALID_TTY_NO_PARAM = + "%s: In %s: No such parameter '%s'"; + public static final String MSGPAT_INVALID_TTY_NOT_BOOL = + "%s: In %s: Parameter '%s' must have bool type"; + public static final String MSGPAT_INVALID_TTY_BAD_VAL = + "%s: In %s: Parameter '%s' has type %s, but '%s' cannot be parsed as such"; public static final String MSGPAT_INVALID_TIMEOUT_SYNTAX = "" + "%s: Invalid %s syntax. Use [milliseconds]"; + public static class ParseException extends Exception { + private Location loc; + + public ParseException(Location loc, String message) { + super(message); + this.loc = loc; + } + + public Location getLocation() { + return loc; + } + } + protected record Location(String fileName, int lineNo) { @Override public String toString() { @@ -80,31 +104,36 @@ public abstract class ScriptAttributesParser { } } - protected interface OptType { - static OptType parse(Location loc, String typeName, - Map> userEnums) { + protected interface OptType extends ValStr.Decoder { + static OptType parse(Location loc, String typeName, Map> userEnums) + throws ParseException { OptType type = BaseType.parseNoErr(typeName); if (type == null) { type = userEnums.get(typeName); } if (type == null) { // still - Msg.error(ScriptAttributesParser.class, - "%s: Invalid type %s".formatted(loc, typeName)); - return null; + throw new ParseException(loc, "%s: Invalid type %s".formatted(loc, typeName)); } return type; } - default TypeAndDefault withCastDefault(Object defaultValue) { - return new TypeAndDefault<>(this, cls().cast(defaultValue)); + default TypeAndDefault withCastDefault(ValStr defaultValue) { + return new TypeAndDefault<>(this, ValStr.cast(cls(), defaultValue)); } Class cls(); - T decode(Location loc, String str); + default T decode(Location loc, String str) throws ParseException { + try { + return decode(str); + } + catch (Exception e) { + throw new ParseException(loc, "%s: %s".formatted(loc, e.getMessage())); + } + } - ParameterDescription createParameter(String name, T defaultValue, String display, - String description); + LaunchParameter createParameter(String name, String display, String description, + boolean required, ValStr defaultValue); } protected interface BaseType extends OptType { @@ -120,12 +149,10 @@ public abstract class ScriptAttributesParser { }; } - public static BaseType parse(Location loc, String typeName) { + public static BaseType parse(Location loc, String typeName) throws ParseException { BaseType type = parseNoErr(typeName); if (type == null) { - Msg.error(ScriptAttributesParser.class, - "%s: Invalid base type %s".formatted(loc, typeName)); - return null; + throw new ParseException(loc, "%s: Invalid base type %s".formatted(loc, typeName)); } return type; } @@ -137,7 +164,7 @@ public abstract class ScriptAttributesParser { } @Override - public String decode(Location loc, String str) { + public String decode(String str) { return str; } }; @@ -149,18 +176,14 @@ public abstract class ScriptAttributesParser { } @Override - public BigInteger decode(Location loc, String str) { + public BigInteger decode(String str) { try { - if (str.startsWith("0x")) { - return new BigInteger(str.substring(2), 16); - } - return new BigInteger(str); + return NumericUtilities.decodeBigInteger(str); } catch (NumberFormatException e) { - Msg.error(ScriptAttributesParser.class, - ("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " + - "Otherwise, decimal is used.").formatted(loc, AT_ENV, str)); - return null; + throw new IllegalArgumentException( + "Invalid int %s. Prefixes 0x, 0b, and 0 (octal) are allowed." + .formatted(str)); } } }; @@ -172,17 +195,16 @@ public abstract class ScriptAttributesParser { } @Override - public Boolean decode(Location loc, String str) { - Boolean result = switch (str) { + public Boolean decode(String str) { + Boolean result = switch (str.trim().toLowerCase()) { case "true" -> true; case "false" -> false; default -> null; }; if (result == null) { - Msg.error(ScriptAttributesParser.class, - "%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed." - .formatted(loc, AT_ENV, str)); - return null; + throw new IllegalArgumentException( + "Invalid bool for %s: %s. Only true or false is allowed." + .formatted(AT_ENV, str)); } return result; } @@ -195,7 +217,7 @@ public abstract class ScriptAttributesParser { } @Override - public Path decode(Location loc, String str) { + public Path decode(String str) { return Paths.get(str); } }; @@ -207,7 +229,7 @@ public abstract class ScriptAttributesParser { } @Override - public PathIsDir decode(Location loc, String str) { + public PathIsDir decode(String str) { return new PathIsDir(Paths.get(str)); } }; @@ -219,7 +241,7 @@ public abstract class ScriptAttributesParser { } @Override - public PathIsFile decode(Location loc, String str) { + public PathIsFile decode(String str) { return new PathIsFile(Paths.get(str)); } }; @@ -228,11 +250,15 @@ public abstract class ScriptAttributesParser { return new UserType<>(this, choices.stream().map(cls()::cast).toList()); } + default UserType withChoices(List choices) { + return new UserType<>(this, choices); + } + @Override - default ParameterDescription createParameter(String name, T defaultValue, String display, - String description) { - return ParameterDescription.create(cls(), name, false, defaultValue, display, - description); + default LaunchParameter createParameter(String name, String display, String description, + boolean required, ValStr defaultValue) { + return LaunchParameter.create(cls(), name, display, description, required, defaultValue, + this); } } @@ -243,62 +269,57 @@ public abstract class ScriptAttributesParser { } @Override - public T decode(Location loc, String str) { - return base.decode(loc, str); + public T decode(String str) { + return base.decode(str); } @Override - public ParameterDescription createParameter(String name, T defaultValue, String display, - String description) { - return ParameterDescription.choices(cls(), name, choices, defaultValue, display, - description); + public LaunchParameter createParameter(String name, String display, String description, + boolean required, ValStr defaultValue) { + return LaunchParameter.choices(cls(), name, display, description, choices, + defaultValue); } } - protected record TypeAndDefault(OptType type, T defaultValue) { + protected record TypeAndDefault(OptType type, ValStr defaultValue) { public static TypeAndDefault parse(Location loc, String typeName, String defaultString, - Map> userEnums) { + Map> userEnums) throws ParseException { OptType tac = OptType.parse(loc, typeName, userEnums); - if (tac == null) { - return null; - } Object value = tac.decode(loc, defaultString); - if (value == null) { - return null; - } - return tac.withCastDefault(value); + return tac.withCastDefault(new ValStr<>(value, defaultString)); } - public ParameterDescription createParameter(String name, String display, - String description) { - return type.createParameter(name, defaultValue, display, description); + public LaunchParameter createParameter(String name, String display, String description) { + return type.createParameter(name, display, description, false, defaultValue); } } public interface TtyCondition { - boolean isActive(Map args); + boolean isActive(Map> args); } enum ConstTtyCondition implements TtyCondition { ALWAYS { @Override - public boolean isActive(Map args) { + public boolean isActive(Map> args) { return true; } }, } - record EqualsTtyCondition(String key, String repr) implements TtyCondition { + record EqualsTtyCondition(LaunchParameter param, Object value) implements TtyCondition { @Override - public boolean isActive(Map args) { - return Objects.toString(args.get(key)).equals(repr); + public boolean isActive(Map> args) { + ValStr valStr = param.get(args); + return Objects.equals(valStr == null ? null : valStr.val(), value); } } - record BoolTtyCondition(String key) implements TtyCondition { + record BoolTtyCondition(LaunchParameter param) implements TtyCondition { @Override - public boolean isActive(Map args) { - return args.get(key) instanceof Boolean b && b.booleanValue(); + public boolean isActive(Map> args) { + ValStr valStr = param.get(args); + return valStr != null && valStr.val(); } } @@ -318,9 +339,8 @@ public abstract class ScriptAttributesParser { public record ScriptAttributes(String title, String description, List menuPath, String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, - Map> parameters, Map extraTtys, - int timeoutMillis, boolean noImage) { - } + Map> parameters, Map extraTtys, + int timeoutMillis, boolean noImage) {} /** * Convert an arguments map into a command line and environment variables @@ -335,34 +355,35 @@ public abstract class ScriptAttributesParser { * @param address the address of the listening TraceRmi socket */ public static void processArguments(List commandLine, Map env, - File script, Map> parameters, Map args, + File script, Map> parameters, Map> args, SocketAddress address) { commandLine.add(script.getAbsolutePath()); - env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath()); + env.put(ENV_GHIDRA_HOME, Application.getInstallationDirectory().getAbsolutePath()); if (address != null) { - env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address)); + env.put(ENV_GHIDRA_TRACE_RMI_ADDR, sockToString(address)); if (address instanceof InetSocketAddress tcp) { - env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().getHostAddress()); - env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort())); + env.put(ENV_GHIDRA_TRACE_RMI_HOST, tcp.getAddress().getHostAddress()); + env.put(ENV_GHIDRA_TRACE_RMI_PORT, Integer.toString(tcp.getPort())); } } - ParameterDescription paramDesc; - for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) { - commandLine.add(Objects.toString(paramDesc.get(args))); + LaunchParameter param; + for (int i = 1; (param = parameters.get("arg:" + i)) != null; i++) { + // Don't use ValStr.str here. I'd like the script's input normalized + commandLine.add(Objects.toString(param.get(args).val())); } - paramDesc = parameters.get("args"); - if (paramDesc != null) { - commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args))); + param = parameters.get("args"); + if (param != null) { + commandLine.addAll(ShellUtils.parseArgs(param.get(args).str())); } - for (Entry> ent : parameters.entrySet()) { + for (Entry> ent : parameters.entrySet()) { String key = ent.getKey(); if (key.startsWith(PREFIX_ENV)) { String varName = key.substring(PREFIX_ENV.length()); - env.put(varName, Objects.toString(ent.getValue().get(args))); + env.put(varName, Objects.toString(ent.getValue().get(args).val())); } } } @@ -376,7 +397,7 @@ public abstract class ScriptAttributesParser { private String iconId; private HelpLocation helpLocation; private final Map> userTypes = new HashMap<>(); - private final Map> parameters = new LinkedHashMap<>(); + private final Map> parameters = new LinkedHashMap<>(); private final Map extraTtys = new LinkedHashMap<>(); private int timeoutMillis = AbstractTraceRmiLaunchOffer.DEFAULT_TIMEOUT_MILLIS; private boolean noImage = false; @@ -401,9 +422,17 @@ public abstract class ScriptAttributesParser { */ protected abstract String removeDelimiter(String line); - public ScriptAttributes parseFile(File script) throws FileNotFoundException { + /** + * Parse the header from the give input stream + * + * @param stream the stream from of the input stream file + * @param scriptName the name of the script file + * @return the parsed attributes + * @throws IOException if there was an issue reading the stream + */ + public ScriptAttributes parseStream(InputStream stream, String scriptName) throws IOException { try (BufferedReader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(script)))) { + new BufferedReader(new InputStreamReader(stream))) { String line; for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) { if (ignoreLine(lineNo, line)) { @@ -413,9 +442,22 @@ public abstract class ScriptAttributesParser { if (comment == null) { break; } - parseComment(new Location(script.getName(), lineNo), comment); + parseComment(new Location(scriptName, lineNo), comment); } - return validate(script.getName()); + return validate(scriptName); + } + } + + /** + * Parse the header of the given script file + * + * @param script the file + * @return the parsed attributes + * @throws FileNotFoundException if the script file could not be found + */ + public ScriptAttributes parseFile(File script) throws FileNotFoundException { + try { + return parseStream(new FileInputStream(script), script.getName()); } catch (FileNotFoundException e) { // Avoid capture by IOException @@ -468,7 +510,7 @@ public abstract class ScriptAttributesParser { protected void parseTitle(Location loc, String str) { if (title != null) { - Msg.warn(this, "%s: Duplicate @title".formatted(loc)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_TITLE)); } title = str; } @@ -483,161 +525,222 @@ public abstract class ScriptAttributesParser { protected void parseMenuPath(Location loc, String str) { if (menuPath != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_MENU_PATH)); } menuPath = List.of(str.trim().split("\\.")); if (menuPath.isEmpty()) { - Msg.error(this, + reportError( "%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH)); } } protected void parseMenuGroup(Location loc, String str) { if (menuGroup != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_MENU_GROUP)); } menuGroup = str; } protected void parseMenuOrder(Location loc, String str) { if (menuOrder != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_MENU_ORDER)); } menuOrder = str; } protected void parseIcon(Location loc, String str) { if (iconId != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_ICON)); } iconId = str.trim(); if (!Gui.hasIcon(iconId)) { - Msg.error(this, + reportError( "%s: Icon id %s not registered in the theme".formatted(loc, iconId)); } } protected void parseHelp(Location loc, String str) { if (helpLocation != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP)); + reportWarning("%s: Duplicate %s".formatted(loc, AT_HELP)); } String[] parts = str.trim().split("#", 2); if (parts.length != 2) { - Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP)); + reportError(MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP)); return; } helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim()); } + protected UserType parseEnumChoices(Location loc, BaseType baseType, + List choiceParts) { + List choices = new ArrayList<>(); + boolean err = false; + for (String s : choiceParts) { + try { + choices.add(baseType.decode(loc, s)); + } + catch (ParseException e) { + reportError(e.getMessage()); + } + } + if (err) { + return null; + } + return baseType.withChoices(choices); + } + protected void parseEnum(Location loc, String str) { List parts = ShellUtils.parseArgs(str); if (parts.size() < 2) { - Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); + reportError(MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); return; } String[] nameParts = parts.get(0).split(":", 2); if (nameParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); + reportError(MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); return; } String name = nameParts[0].trim(); - BaseType baseType = BaseType.parse(loc, nameParts[1]); - if (baseType == null) { + BaseType baseType; + try { + baseType = BaseType.parse(loc, nameParts[1]); + } + catch (ParseException e) { + reportError(e.getMessage()); return; } - List choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList(); - if (choices.contains(null)) { - return; + UserType userType = parseEnumChoices(loc, baseType, parts.subList(1, parts.size())); + if (userType == null) { + return; // errors already reported } - UserType userType = baseType.withCastChoices(choices); if (userTypes.put(name, userType) != null) { - Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name)); + reportWarning("%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name)); } } protected void parseEnv(Location loc, String str) { List parts = ShellUtils.parseArgs(str); if (parts.size() != 3) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + reportError(MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); return; } String[] nameParts = parts.get(0).split(":", 2); if (nameParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + reportError(MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); return; } String trimmed = nameParts[0].trim(); String name = PREFIX_ENV + trimmed; String[] tadParts = nameParts[1].split("=", 2); if (tadParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + reportError(MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); return; } - TypeAndDefault tad = - TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes); - ParameterDescription param = tad.createParameter(name, parts.get(1), parts.get(2)); - if (parameters.put(name, param) != null) { - Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed)); + try { + TypeAndDefault tad = + TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes); + LaunchParameter param = tad.createParameter(name, parts.get(1), parts.get(2)); + if (parameters.put(name, param) != null) { + reportWarning("%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed)); + } + } + catch (ParseException e) { + reportError(e.getMessage()); } } protected void parseArg(Location loc, String str, int argNum) { List parts = ShellUtils.parseArgs(str); if (parts.size() != 3) { - Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); + reportError(MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); return; } String colonType = parts.get(0).trim(); if (!colonType.startsWith(":")) { - Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); + reportError(MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); return; } - OptType type = OptType.parse(loc, colonType.substring(1), userTypes); - if (type == null) { - return; + OptType type; + try { + type = OptType.parse(loc, colonType.substring(1), userTypes); + String name = PREFIX_ARG + argNum; + parameters.put(name, + type.createParameter(name, parts.get(1), parts.get(2), true, + new ValStr<>(null, ""))); + } + catch (ParseException e) { + reportError(e.getMessage()); } - String name = PREFIX_ARG + argNum; - parameters.put(name, ParameterDescription.create(type.cls(), name, true, null, - parts.get(1), parts.get(2))); } protected void parseArgs(Location loc, String str) { List parts = ShellUtils.parseArgs(str); if (parts.size() != 2) { - Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS)); + reportError(MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS)); return; } - ParameterDescription parameter = ParameterDescription.create(String.class, - "args", false, "", parts.get(0), parts.get(1)); + + LaunchParameter parameter = BaseType.STRING.createParameter( + "args", parts.get(0), parts.get(1), false, ValStr.str("")); if (parameters.put(KEY_ARGS, parameter) != null) { - Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS)); + reportWarning("%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS)); } } protected void putTty(Location loc, String name, TtyCondition condition) { if (extraTtys.put(name, condition) != null) { - Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY)); + reportWarning("%s: Duplicate %s. Ignored".formatted(loc, AT_TTY)); } } protected void parseTty(Location loc, String str) { List parts = ShellUtils.parseArgs(str); switch (parts.size()) { - case 1: + case 1 -> { putTty(loc, parts.get(0), ConstTtyCondition.ALWAYS); return; - case 3: + } + case 3 -> { if ("if".equals(parts.get(1))) { - putTty(loc, parts.get(0), new BoolTtyCondition(parts.get(2))); + LaunchParameter param = parameters.get(parts.get(2)); + if (param == null) { + reportError( + MSGPAT_INVALID_TTY_NO_PARAM.formatted(loc, AT_TTY, parts.get(2))); + return; + } + if (param.type() != Boolean.class) { + reportError( + MSGPAT_INVALID_TTY_NOT_BOOL.formatted(loc, AT_TTY, param.name())); + return; + } + @SuppressWarnings("unchecked") + LaunchParameter asBoolParam = (LaunchParameter) param; + putTty(loc, parts.get(0), new BoolTtyCondition(asBoolParam)); return; } - case 5: + } + case 5 -> { if ("if".equals(parts.get(1)) && "==".equals(parts.get(3))) { - putTty(loc, parts.get(0), new EqualsTtyCondition(parts.get(2), parts.get(4))); - return; + LaunchParameter param = parameters.get(parts.get(2)); + if (param == null) { + reportError( + MSGPAT_INVALID_TTY_NO_PARAM.formatted(loc, AT_TTY, parts.get(2))); + return; + } + try { + Object value = param.decode(parts.get(4)).val(); + putTty(loc, parts.get(0), new EqualsTtyCondition(param, value)); + return; + } + catch (Exception e) { + reportError(MSGPAT_INVALID_TTY_BAD_VAL.formatted(loc, AT_TTY, + param.name(), param.type(), parts.get(4))); + return; + } } + } } - Msg.error(this, MSGPAT_INVALID_TTY_SYNTAX.formatted(loc, AT_TTY)); + reportError(MSGPAT_INVALID_TTY_SYNTAX.formatted(loc, AT_TTY)); } protected void parseTimeout(Location loc, String str) { @@ -645,7 +748,7 @@ public abstract class ScriptAttributesParser { timeoutMillis = Integer.parseInt(str); } catch (NumberFormatException e) { - Msg.error(this, MSGPAT_INVALID_TIMEOUT_SYNTAX.formatted(loc, AT_TIMEOUT)); + reportError(MSGPAT_INVALID_TIMEOUT_SYNTAX.formatted(loc, AT_TIMEOUT)); } } @@ -654,12 +757,13 @@ public abstract class ScriptAttributesParser { } protected void parseUnrecognized(Location loc, String line) { - Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line)); + reportWarning("%s: Unrecognized metadata: %s".formatted(loc, line)); } protected ScriptAttributes validate(String fileName) { if (title == null) { - Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE)); + reportError( + "%s is required. Using script file name: '%s'".formatted(AT_TITLE, fileName)); title = fileName; } if (menuPath == null) { @@ -683,4 +787,12 @@ public abstract class ScriptAttributesParser { private String getDescription() { return description == null ? null : description.toString(); } + + protected void reportWarning(String message) { + Msg.warn(this, message); + } + + protected void reportError(String message) { + Msg.error(this, message); + } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialog.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialog.java new file mode 100644 index 0000000000..70885a0fcb --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialog.java @@ -0,0 +1,86 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.util.List; +import java.util.Map; + +import javax.swing.Icon; + +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.LaunchParameter; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.PluginTool; + +public class TraceRmiLaunchDialog extends AbstractDebuggerParameterDialog> { + + public TraceRmiLaunchDialog(PluginTool tool, String title, String buttonText, Icon buttonIcon) { + super(tool, title, buttonText, buttonIcon); + } + + @Override + protected String parameterName(LaunchParameter parameter) { + return parameter.name(); + } + + @Override + protected Class parameterType(LaunchParameter parameter) { + return parameter.type(); + } + + @Override + protected String parameterLabel(LaunchParameter parameter) { + return parameter.display(); + } + + @Override + protected String parameterToolTip(LaunchParameter parameter) { + return parameter.description(); + } + + @Override + protected ValStr parameterDefault(LaunchParameter parameter) { + return parameter.defaultValue(); + } + + @Override + protected List parameterChoices(LaunchParameter parameter) { + return parameter.choices(); + } + + @Override + protected Map> validateArguments(Map> parameters, + Map> arguments) { + return LaunchParameter.validateArguments(parameters, arguments); + } + + @Override + protected void parameterSaveValue(LaunchParameter parameter, SaveState state, String key, + ValStr value) { + state.putString(key, value.str()); + } + + @Override + protected ValStr parameterLoadValue(LaunchParameter parameter, SaveState state, + String key) { + String str = state.getString(key, null); + if (str == null) { + return null; + } + return parameter.decode(str); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java index 052b3d9dfa..e6175c2f3b 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.debug.api.ValStr; import ghidra.program.model.listing.Program; /** @@ -32,6 +33,8 @@ import ghidra.program.model.listing.Program; * {@link ScriptAttributesParser}. */ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer { + public static final String HASH = "#"; + public static final int HASH_LEN = HASH.length(); public static final String SHEBANG = "#!"; /** @@ -56,10 +59,10 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLa @Override protected String removeDelimiter(String line) { String stripped = line.stripLeading(); - if (!stripped.startsWith("#")) { + if (!stripped.startsWith(HASH)) { return null; } - return stripped.substring(1); + return stripped.substring(HASH_LEN); } }; ScriptAttributes attrs = parser.parseFile(script); @@ -68,15 +71,7 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLa } private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, - Program program, - File script, String configName, ScriptAttributes attrs) { + Program program, File script, String configName, ScriptAttributes attrs) { super(plugin, program, script, configName, attrs); } - - @Override - protected void prepareSubprocess(List commandLine, Map env, - Map args, SocketAddress address) { - ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args, - address); - } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java index 6750bd9f1a..721862cefb 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java @@ -54,7 +54,6 @@ import ghidra.program.model.address.*; import ghidra.program.model.lang.*; import ghidra.program.util.DefaultLanguageService; import ghidra.rmi.trace.TraceRmi.*; -import ghidra.rmi.trace.TraceRmi.Compiler; import ghidra.rmi.trace.TraceRmi.Language; import ghidra.trace.database.DBTrace; import ghidra.trace.model.Lifespan; @@ -129,11 +128,9 @@ public class TraceRmiHandler implements TraceRmiConnection { } } - protected record Tid(DoId doId, int txId) { - } + protected record Tid(DoId doId, int txId) {} - protected record OpenTx(Tid txId, Transaction tx, boolean undoable) { - } + protected record OpenTx(Tid txId, Transaction tx, boolean undoable) {} protected class OpenTraceMap { private final Map byId = new HashMap<>(); @@ -388,7 +385,7 @@ public class TraceRmiHandler implements TraceRmiConnection { protected static void sendDelimited(OutputStream out, RootMessage msg, long dbgSeq) throws IOException { - ByteBuffer buf = ByteBuffer.allocate(4); + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); buf.putInt(msg.getSerializedSize()); out.write(buf.array()); msg.writeTo(out); @@ -867,6 +864,9 @@ public class TraceRmiHandler implements TraceRmiConnection { throws InvalidNameException, IOException, CancelledException { DomainFolder traces = getOrCreateNewTracesFolder(); List path = sanitizePath(req.getPath().getPath()); + if (path.isEmpty()) { + throw new IllegalArgumentException("CreateTrace: path (name) cannot be empty"); + } DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1)); CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler()); DBTrace trace = new DBTrace(path.get(path.size() - 1), cs, this); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java index b255eb991e..673c3a444b 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,6 +37,7 @@ import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.dbg.util.PathMatcher; import ghidra.dbg.util.PathPredicates; import ghidra.dbg.util.PathPredicates.Align; +import ghidra.debug.api.ValStr; import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.model.DebuggerSingleObjectPathActionContext; import ghidra.debug.api.target.ActionName; @@ -345,25 +346,15 @@ public class TraceRmiTarget extends AbstractTarget { } private Map promptArgs(RemoteMethod method, Map defaults) { - SchemaContext ctx = getSchemaContext(); + /** + * TODO: RemoteMethod parameter descriptions should also use ValStr. This map conversion + * stuff is getting onerous and hacky. + */ + Map> defs = ValStr.fromPlainMap(defaults); RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool, - method.display(), method.display(), null); - while (true) { - for (RemoteParameter param : method.parameters().values()) { - Object val = defaults.get(param.name()); - if (val != null) { - Class type = ctx.getSchema(param.type()).getType(); - dialog.setMemorizedArgument(param.name(), type.asSubclass(Object.class), - val); - } - } - Map args = dialog.promptArguments(ctx, method.parameters(), defaults); - if (args == null) { - // Cancelled - return null; - } - return args; - } + getSchemaContext(), method.display(), method.display(), null); + Map> args = dialog.promptArguments(method.parameters(), defs, defs); + return args == null ? null : ValStr.toPlainMap(args); } private CompletableFuture invokeMethod(boolean prompt, RemoteMethod method, diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java index fbcd3d62f8..529e0c8e97 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,7 +30,7 @@ import org.junit.Test; import generic.Unique; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; -import ghidra.app.plugin.core.debug.gui.objects.components.InvocationDialogHelper; +import ghidra.app.plugin.core.debug.gui.InvocationDialogHelper; import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*; import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin; import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient; @@ -60,13 +60,17 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD provider = waitForComponentProvider(TraceRmiConnectionManagerProvider.class); } + InvocationDialogHelper waitDialog() { + return InvocationDialogHelper.waitFor(TraceRmiConnectDialog.class); + } + @Test public void testActionAccept() throws Exception { performEnabledAction(provider, provider.actionConnectAccept, false); - InvocationDialogHelper helper = InvocationDialogHelper.waitFor(); + InvocationDialogHelper helper = waitDialog(); helper.dismissWithArguments(Map.ofEntries( - Map.entry("address", "localhost"), - Map.entry("port", 0))); + helper.entry("address", "localhost"), + helper.entry("port", 0))); waitForPass(() -> Unique.assertOne(traceRmiService.getAllAcceptors())); } @@ -78,10 +82,10 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD throw new AssertionError(); } performEnabledAction(provider, provider.actionConnectOutbound, false); - InvocationDialogHelper helper = InvocationDialogHelper.waitFor(); + InvocationDialogHelper helper = waitDialog(); helper.dismissWithArguments(Map.ofEntries( - Map.entry("address", sockaddr.getHostString()), - Map.entry("port", sockaddr.getPort()))); + helper.entry("address", sockaddr.getHostString()), + helper.entry("port", sockaddr.getPort()))); try (SocketChannel channel = server.accept()) { TestTraceRmiClient client = new TestTraceRmiClient(channel); client.sendNegotiate("Test client"); @@ -94,10 +98,10 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD @Test public void testActionStartServer() throws Exception { performEnabledAction(provider, provider.actionStartServer, false); - InvocationDialogHelper helper = InvocationDialogHelper.waitFor(); + InvocationDialogHelper helper = waitDialog(); helper.dismissWithArguments(Map.ofEntries( - Map.entry("address", "localhost"), - Map.entry("port", 0))); + helper.entry("address", "localhost"), + helper.entry("port", 0))); waitForPass(() -> assertTrue(traceRmiService.isServerStarted())); waitForPass(() -> assertFalse(provider.actionStartServer.isEnabled())); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerParameterDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerParameterDialog.java new file mode 100644 index 0000000000..8b229e15af --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/AbstractDebuggerParameterDialog.java @@ -0,0 +1,767 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.beans.*; +import java.io.File; +import java.math.BigInteger; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.List; +import java.util.Map.Entry; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.text.StringEscapeUtils; + +import docking.DialogComponentProvider; +import docking.options.editor.FileChooserEditor; +import docking.widgets.button.BrowseButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils; +import ghidra.debug.api.ValStr; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.AutoConfigState.PathIsDir; +import ghidra.framework.plugintool.AutoConfigState.PathIsFile; +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.*; +import ghidra.util.layout.PairLayout; + +public abstract class AbstractDebuggerParameterDialog

extends DialogComponentProvider + implements PropertyChangeListener { + static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments"; + + public static class BigIntEditor extends PropertyEditorSupport { + String asText = ""; + + @Override + public String getJavaInitializationString() { + Object value = getValue(); + return value == null + ? "null" + : "new BigInteger(\"%s\")".formatted(value); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + /** + * Set asText first, since setValue will fire change listener. It will call getAsText(). + */ + asText = text; + setValueNoAsText(text == null + ? null + : NumericUtilities.decodeBigInteger(text)); + } + + public void setValueNoAsText(Object value) { + super.setValue(value); + } + + @Override + public void setValue(Object value) { + super.setValue(value); + asText = value == null ? "" : value.toString(); + } + + @Override + public String getAsText() { + return asText; + } + } + + public static class FileChooserPanel extends JPanel { + private final static int NUMBER_OF_COLUMNS = 20; + + private final JTextField textField = new JTextField(NUMBER_OF_COLUMNS); + private final JButton browseButton = new BrowseButton(); + private final Runnable propertyChange; + + private GhidraFileChooser fileChooser; // lazy + + public FileChooserPanel(Runnable propertyChange) { + this.propertyChange = propertyChange; + + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + add(textField); + add(Box.createHorizontalStrut(5)); + add(browseButton); + setBorder(BorderFactory.createEmptyBorder()); + + textField.addActionListener(e -> propertyChange.run()); + textField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent e) { + propertyChange.run(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + propertyChange.run(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + propertyChange.run(); + } + }); + + browseButton.addActionListener(e -> displayFileChooser()); + } + + public void setValue(File file) { + textField.setText(file == null ? "" : file.getAbsolutePath()); + } + + private void displayFileChooser() { + if (fileChooser == null) { + fileChooser = createFileChooser(); + } + + String path = textField.getText().trim(); + if (!path.isEmpty()) { + File f = new File(path); + if (f.isDirectory()) { + fileChooser.setCurrentDirectory(f); + } + else { + File pf = f.getParentFile(); + if (pf != null && pf.isDirectory()) { + fileChooser.setSelectedFile(f); + } + } + } + + File chosen = fileChooser.getSelectedFile(true); + if (chosen != null) { + textField.setText(chosen.getAbsolutePath()); + propertyChange.run(); + } + } + + protected String getTitle() { + return "Choose Path"; + } + + protected GhidraFileChooserMode getSelectionMode() { + return GhidraFileChooserMode.FILES_AND_DIRECTORIES; + } + + private GhidraFileChooser createFileChooser() { + GhidraFileChooser chooser = new GhidraFileChooser(browseButton); + chooser.setTitle(getTitle()); + chooser.setApproveButtonText(getTitle()); + chooser.setFileSelectionMode(getSelectionMode()); + // No way for script to specify filter.... + + return chooser; + } + } + + /** + * Compared to {@link FileChooserEditor}, this does not require the user to enter a full path. + * Nor will it resolve file names against the working directory. It's just a text box with a + * file browser assist. + */ + public static class PathEditor extends PropertyEditorSupport { + private final FileChooserPanel panel = newChooserPanel(); + + protected FileChooserPanel newChooserPanel() { + return new FileChooserPanel(this::firePropertyChange); + } + + @Override + public String getAsText() { + return panel.textField.getText().trim(); + } + + @Override + public Object getValue() { + String text = panel.textField.getText().trim(); + if (text.isEmpty()) { + return null; + } + return Paths.get(text); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (text == null || text.isBlank()) { + panel.textField.setText(""); + } + else { + panel.textField.setText(text); + } + } + + @Override + public void setValue(Object value) { + if (value == null) { + panel.textField.setText(""); + } + else if (value instanceof String s) { + panel.textField.setText(s); + } + else if (value instanceof Path p) { + panel.textField.setText(p.toString()); + } + else { + throw new IllegalArgumentException("value=" + value); + } + } + + @Override + public boolean supportsCustomEditor() { + return true; + } + + @Override + public Component getCustomEditor() { + return panel; + } + } + + public static class PathIsDirEditor extends PathEditor { + @Override + protected FileChooserPanel newChooserPanel() { + return new FileChooserPanel(this::firePropertyChange) { + @Override + protected String getTitle() { + return "Choose Directory"; + } + + @Override + protected GhidraFileChooserMode getSelectionMode() { + return GhidraFileChooserMode.DIRECTORIES_ONLY; + } + }; + } + + @Override + public Object getValue() { + Object value = super.getValue(); + if (value == null) { + return null; + } + if (value instanceof Path p) { + return new PathIsDir(p); + } + throw new AssertionError(); + } + + @Override + public void setValue(Object value) { + if (value instanceof PathIsDir dir) { + super.setValue(dir.path()); + } + else { + super.setValue(value); + } + } + } + + public static class PathIsFileEditor extends PathEditor { + @Override + protected FileChooserPanel newChooserPanel() { + return new FileChooserPanel(this::firePropertyChange) { + @Override + protected String getTitle() { + return "Choose File"; + } + + @Override + protected GhidraFileChooserMode getSelectionMode() { + return GhidraFileChooserMode.FILES_ONLY; + } + }; + } + + @Override + public Object getValue() { + Object value = super.getValue(); + if (value == null) { + return null; + } + if (value instanceof Path p) { + return new PathIsFile(p); + } + throw new AssertionError(); + } + + @Override + public void setValue(Object value) { + if (value instanceof PathIsFile file) { + super.setValue(file.path()); + } + else { + super.setValue(value); + } + } + } + + static { + PropertyEditorManager.registerEditor(BigInteger.class, BigIntEditor.class); + PropertyEditorManager.registerEditor(Path.class, PathEditor.class); + PropertyEditorManager.registerEditor(PathIsDir.class, PathIsDirEditor.class); + PropertyEditorManager.registerEditor(PathIsFile.class, PathIsFileEditor.class); + } + + static class ChoicesPropertyEditor implements PropertyEditor { + private final List choices; + private final String[] tags; + + private final List listeners = new ArrayList<>(); + + private Object value; + + public ChoicesPropertyEditor(Collection choices) { + this.choices = choices.stream().distinct().toList(); + this.tags = choices.stream().map(Objects::toString).toArray(String[]::new); + } + + @Override + public void setValue(Object value) { + if (Objects.equals(value, this.value)) { + return; + } + if (!choices.contains(value)) { + throw new IllegalArgumentException("Unsupported value: " + value); + } + Object oldValue; + List listeners; + synchronized (this.listeners) { + oldValue = this.value; + this.value = value; + if (this.listeners.isEmpty()) { + return; + } + listeners = List.copyOf(this.listeners); + } + PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value); + for (PropertyChangeListener l : listeners) { + l.propertyChange(evt); + } + } + + @Override + public Object getValue() { + return value; + } + + @Override + public boolean isPaintable() { + return false; + } + + @Override + public void paintValue(Graphics gfx, Rectangle box) { + // Not paintable + } + + @Override + public String getJavaInitializationString() { + if (value == null) { + return "null"; + } + if (value instanceof String str) { + return "\"" + StringEscapeUtils.escapeJava(str) + "\""; + } + return Objects.toString(value); + } + + @Override + public String getAsText() { + return Objects.toString(value); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + int index = ArrayUtils.indexOf(tags, text); + if (index < 0) { + throw new IllegalArgumentException("Unsupported value: " + text); + } + setValue(choices.get(index)); + } + + @Override + public String[] getTags() { + return tags.clone(); + } + + @Override + public Component getCustomEditor() { + return null; + } + + @Override + public boolean supportsCustomEditor() { + return false; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + } + + protected record NameTypePair(String name, Class type) { + public static NameTypePair fromString(String name) throws ClassNotFoundException { + String[] parts = name.split(",", 2); + if (parts.length != 2) { + // This appears to be a bad assumption - empty fields results in solitary labels + return new NameTypePair(parts[0], String.class); + //throw new IllegalArgumentException("Could not parse name,type"); + } + return new NameTypePair(parts[0], Class.forName(parts[1])); + } + + public final String encodeString() { + return name + "," + type.getName(); + } + } + + private final BidiMap paramEditors = new DualLinkedHashBidiMap<>(); + + private JPanel panel; + private JLabel descriptionLabel; + private JPanel pairPanel; + private PairLayout layout; + + protected JButton invokeButton; + protected JButton resetButton; + + private final PluginTool tool; + // package access for testing + Map parameters; + + private Map> defaults = Map.of(); + // TODO: Not sure this is the best keying, but I think it works. + private Map> memorized = new HashMap<>(); + private Map> arguments; + + public AbstractDebuggerParameterDialog(PluginTool tool, String title, String buttonText, + Icon buttonIcon) { + super(title, true, true, true, false); + this.tool = tool; + + populateComponents(buttonText, buttonIcon); + setRememberSize(false); + } + + protected abstract String parameterName(P parameter); + + protected abstract Class parameterType(P parameter); + + protected NameTypePair parameterNameAndType(P parameter) { + return new NameTypePair(parameterName(parameter), parameterType(parameter)); + } + + protected abstract String parameterLabel(P parameter); + + protected abstract String parameterToolTip(P parameter); + + protected abstract ValStr parameterDefault(P parameter); + + protected abstract Collection parameterChoices(P parameter); + + protected abstract Map> validateArguments(Map parameters, + Map> arguments); + + protected abstract void parameterSaveValue(P parameter, SaveState state, String key, + ValStr value); + + protected abstract ValStr parameterLoadValue(P parameter, SaveState state, String key); + + protected ValStr computeInitialValue(P parameter) { + ValStr val = memorized.computeIfAbsent(parameterNameAndType(parameter), + ntp -> defaults.get(parameterName(parameter))); + return val; + } + + /** + * Prompt the user for the given arguments, all at once + * + *

+ * This displays a single dialog with each option listed. The parameter map contains the + * description of each parameter to be displayed. The {@code initial} values are the values to + * pre-populate the options with, e.g., because they are saved from a previous session, or + * because they are the suggested values. If the user clicks the "Reset" button, the values are + * revered to the defaults given in each parameter's description, unless that value is + * overridden in {@code defaults}. This may be appropriate if a value is suggested for a + * (perhaps required) option that otherwise has no default. + * + * @param parameterMap the map of parameters, keyed by {@link #parameterName(Object)}. This map + * may be ordered to control the order of options displayed. + * @param initial the initial values of the options. If a key is not provided, the initial value + * is its default value. Extraneous keys are ignored. + * @param defaults the default values to use upon reset. If a key is not provided, the default + * is taken from the parameter description. Extraneous keys are ignored. + * @return the arguments provided by the user + */ + public Map> promptArguments(Map parameterMap, + Map> initial, Map> defaults) { + setDefaults(defaults); + setParameters(parameterMap); + setMemorizedArguments(initial); + populateValues(initial); + tool.showDialog(this); + + return getArguments(); + } + + protected void setParameters(Map parameterMap) { + this.parameters = parameterMap; + for (P param : parameterMap.values()) { + if (!defaults.containsKey(parameterName(param))) { + defaults.put(parameterName(param), parameterDefault(param)); + } + } + populateOptions(); + } + + protected void setMemorizedArguments(Map> initial) { + for (P param : parameters.values()) { + ValStr val = initial.get(parameterName(param)); + if (val != null) { + setMemorizedArgument(param, val); + } + } + } + + protected void setDefaults(Map> defaults) { + this.defaults = new HashMap<>(defaults); + } + + private void populateComponents(String buttonText, Icon buttonIcon) { + panel = new JPanel(new BorderLayout()); + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + layout = new PairLayout(5, 5); + pairPanel = new JPanel(layout); + + JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER)); + JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + //scrolling.setPreferredSize(new Dimension(100, 130)); + panel.add(scrolling, BorderLayout.CENTER); + centering.add(pairPanel); + + descriptionLabel = new JLabel(); + descriptionLabel.setMaximumSize(new Dimension(300, 100)); + panel.add(descriptionLabel, BorderLayout.NORTH); + + addWorkPanel(panel); + + invokeButton = new JButton(buttonText, buttonIcon); + addButton(invokeButton); + resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH); + addButton(resetButton); + addCancelButton(); + + invokeButton.addActionListener(this::invoke); + resetButton.addActionListener(this::reset); + } + + @Override + protected void cancelCallback() { + this.arguments = null; + close(); + } + + void invoke(ActionEvent evt) { + try { + this.arguments = validateArguments(parameters, collectArguments()); + close(); + } + catch (IllegalStateException e) { + setStatusText(e.getMessage(), MessageType.ERROR, true); + } + } + + void reset(ActionEvent evt) { + this.arguments = null; + populateValues(defaults); + } + + protected PropertyEditor createEditor(P parameter) { + Collection choices = parameterChoices(parameter); + if (!choices.isEmpty()) { + return new ChoicesPropertyEditor(choices); + } + Class type = parameterType(parameter); + PropertyEditor editor = PropertyEditorManager.findEditor(type); + if (editor != null) { + return editor; + } + Msg.warn(this, "No editor for " + type + "? Trying String instead"); + editor = PropertyEditorManager.findEditor(String.class); + return editor; + } + + // test access + PropertyEditor getEditor(P parameter) { + return paramEditors.get(parameter); + } + + protected void setEditorValue(PropertyEditor editor, P param, ValStr val) { + switch (val.val()) { + case null -> { + } + case BigInteger bi -> editor.setAsText(val.str()); + default -> editor.setValue(val.val()); + } + } + + void populateOptions() { + pairPanel.removeAll(); + paramEditors.clear(); + for (P param : parameters.values()) { + JLabel label = new JLabel(parameterLabel(param)); + label.setToolTipText(parameterToolTip(param)); + pairPanel.add(label); + + PropertyEditor editor = createEditor(param); + ValStr val = computeInitialValue(param); + setEditorValue(editor, param, val); + editor.addPropertyChangeListener(this); + pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); + paramEditors.put(param, editor); + } + } + + void populateValues(Map> values) { + for (Map.Entry> ent : values.entrySet()) { + P param = parameters.get(ent.getKey()); + if (param == null) { + continue; + } + PropertyEditor editor = paramEditors.get(param); + setEditorValue(editor, param, ent.getValue()); + } + } + + protected Map> collectArguments() { + Map> map = new LinkedHashMap<>(); + Set invalid = new LinkedHashSet<>(); + for (Entry ent : paramEditors.entrySet()) { + P param = ent.getKey(); + PropertyEditor editor = ent.getValue(); + ValStr val = memorized.get(parameterNameAndType(param)); + if (!Objects.equals(editor.getAsText(), val.str())) { + invalid.add(parameterLabel(param)); + } + if (val != null) { + map.put(parameterName(param), val); + } + } + if (!invalid.isEmpty()) { + throw new IllegalStateException("Invalid value for " + invalid); + } + return map; + } + + public Map> getArguments() { + return arguments; + } + + void setMemorizedArgument(P parameter, ValStr value) { + if (value == null) { + return; + } + memorized.put(parameterNameAndType(parameter), value); + } + + public void forgetMemorizedArguments() { + memorized.clear(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + PropertyEditor editor = (PropertyEditor) evt.getSource(); + P param = paramEditors.getKey(editor); + memorized.put(parameterNameAndType(param), + new ValStr<>(editor.getValue(), editor.getAsText())); + } + + public void writeConfigState(SaveState saveState) { + SaveState subState = new SaveState(); + for (Map.Entry> ent : memorized.entrySet()) { + NameTypePair ntp = ent.getKey(); + P param = parameters.get(ntp.name()); + if (param == null) { + continue; + } + parameterSaveValue(param, subState, ntp.encodeString(), ent.getValue()); + } + saveState.putSaveState(KEY_MEMORIZED_ARGUMENTS, subState); + } + + public void readConfigState(SaveState saveState) { + /** + * TODO: This method is defunct. It is only used by the DebuggerObjectsProvider, which is + * now deprecated, but I suspect other providers intend to use this in the same way. If + * those providers don't manually load/compute initial and default values at the time of + * prompting, then this will need to be fixed. The decode of the values will need to be + * delayed until (and repeated every time) parameters are populated. + */ + SaveState subState = saveState.getSaveState(KEY_MEMORIZED_ARGUMENTS); + if (subState == null) { + return; + } + for (String name : subState.getNames()) { + try { + NameTypePair ntp = NameTypePair.fromString(name); + P param = parameters.get(ntp.name()); + if (param == null) { + continue; + } + memorized.put(ntp, parameterLoadValue(param, subState, ntp.encodeString())); + } + catch (Exception e) { + Msg.error(this, "Error restoring memorized parameter " + name, e); + } + } + } + + public void setDescription(String htmlDescription) { + if (htmlDescription == null) { + descriptionLabel.setBorder(BorderFactory.createEmptyBorder()); + descriptionLabel.setText(""); + } + else { + descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + descriptionLabel.setText(htmlDescription); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/ByModuleAutoMapSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/ByModuleAutoMapSpec.java index eeae1b1771..ff8a5234a8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/ByModuleAutoMapSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/ByModuleAutoMapSpec.java @@ -40,6 +40,18 @@ import ghidra.util.task.TaskMonitor; public class ByModuleAutoMapSpec implements AutoMapSpec { public static final String CONFIG_NAME = "1_MAP_BY_MODULE"; + /** + * Get the instance. + * + *

+ * Note this will not work until after the class searcher is done. + * + * @return the instance + */ + public static ByModuleAutoMapSpec instance() { + return (ByModuleAutoMapSpec) AutoMapSpec.fromConfigName(CONFIG_NAME); + } + @Override public String getConfigName() { return CONFIG_NAME; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index 843428f55c..8dac3586de 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.stream.Collectors; import javax.swing.JComponent; import javax.swing.JPanel; @@ -63,6 +64,7 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.util.DebuggerCallbackReorderer; import ghidra.dbg.util.PathUtils; +import ghidra.debug.api.ValStr; import ghidra.debug.api.model.DebuggerMemoryMapper; import ghidra.debug.api.model.TraceRecorder; import ghidra.debug.api.tracemgr.DebuggerCoordinates; @@ -135,8 +137,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter private final AutoService.Wiring autoServiceWiring; @AutoOptionDefined( - name = "Default Extended Step", - description = "The default string for the extended step command") + name = "Default Extended Step", + description = "The default string for the extended step command") String extendedStep = ""; @SuppressWarnings("unused") @@ -1367,13 +1369,14 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter boolean prompt = p; }; return AsyncUtils.loop(TypeSpec.VOID, (loop) -> { - Map args = launchOffer.getLauncherArgs(launcher, locals.prompt); + Map> args = launchOffer.getLauncherArgs(launcher, locals.prompt); if (args == null) { // Cancelled loop.exit(); } else { - launcher.launch(args).thenAccept(loop::exit).exceptionally(ex -> { + Map a = ValStr.toPlainMap(args); + launcher.launch(a).thenAccept(loop::exit).exceptionally(ex -> { loop.repeat(); return null; }); @@ -1428,7 +1431,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter } return; } - Map args = methodDialog.promptArguments(parameters); + Map args = methodDialog.promptArguments(parameters, Map.of(), Map.of()); if (args != null) { String script = (String) args.get("Script"); if (script != null && !script.isEmpty()) { @@ -1623,7 +1626,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter if (configParameters.isEmpty()) { return AsyncUtils.nil(); } - Map args = configDialog.promptArguments(configParameters); + Map args = + configDialog.promptArguments(configParameters, Map.of(), Map.of()); if (args == null) { // User cancelled return AsyncUtils.nil(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java index 090a717c3f..0f111d2ea8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,654 +15,75 @@ */ package ghidra.app.plugin.core.debug.gui.objects.components; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.beans.*; -import java.io.File; -import java.math.BigInteger; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; +import javax.swing.Icon; -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.tuple.MutablePair; -import org.apache.commons.text.StringEscapeUtils; -import org.jdom.Element; - -import docking.DialogComponentProvider; -import docking.options.editor.FileChooserEditor; -import docking.widgets.button.BrowseButton; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils; +import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog; import ghidra.dbg.target.TargetMethod; import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.debug.api.ValStr; import ghidra.framework.options.SaveState; -import ghidra.framework.plugintool.AutoConfigState.*; +import ghidra.framework.plugintool.AutoConfigState.ConfigStateField; import ghidra.framework.plugintool.PluginTool; -import ghidra.util.Msg; -import ghidra.util.layout.PairLayout; -public class DebuggerMethodInvocationDialog extends DialogComponentProvider - implements PropertyChangeListener { - - public static class BigIntEditor extends PropertyEditorSupport { - @Override - public String getJavaInitializationString() { - Object value = getValue(); - return value == null - ? "null" - : "new BigInteger(\"%s\")".formatted(value); - } - - @Override - public void setAsText(String text) throws IllegalArgumentException { - setValue(text == null - ? null - : new BigInteger(text)); - } - } - - public static class FileChooserPanel extends JPanel { - private final static int NUMBER_OF_COLUMNS = 20; - - private final JTextField textField = new JTextField(NUMBER_OF_COLUMNS); - private final JButton browseButton = new BrowseButton(); - private final Runnable propertyChange; - - private GhidraFileChooser fileChooser; // lazy - - public FileChooserPanel(Runnable propertyChange) { - this.propertyChange = propertyChange; - - setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); - add(textField); - add(Box.createHorizontalStrut(5)); - add(browseButton); - setBorder(BorderFactory.createEmptyBorder()); - - textField.addActionListener(e -> propertyChange.run()); - textField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void removeUpdate(DocumentEvent e) { - propertyChange.run(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - propertyChange.run(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - propertyChange.run(); - } - }); - - browseButton.addActionListener(e -> displayFileChooser()); - } - - public void setValue(File file) { - textField.setText(file == null ? "" : file.getAbsolutePath()); - } - - private void displayFileChooser() { - if (fileChooser == null) { - fileChooser = createFileChooser(); - } - - String path = textField.getText().trim(); - if (!path.isEmpty()) { - File f = new File(path); - if (f.isDirectory()) { - fileChooser.setCurrentDirectory(f); - } - else { - File pf = f.getParentFile(); - if (pf != null && pf.isDirectory()) { - fileChooser.setSelectedFile(f); - } - } - } - - File chosen = fileChooser.getSelectedFile(true); - if (chosen != null) { - textField.setText(chosen.getAbsolutePath()); - propertyChange.run(); - } - } - - protected String getTitle() { - return "Choose Path"; - } - - protected GhidraFileChooserMode getSelectionMode() { - return GhidraFileChooserMode.FILES_AND_DIRECTORIES; - } - - private GhidraFileChooser createFileChooser() { - GhidraFileChooser chooser = new GhidraFileChooser(browseButton); - chooser.setTitle(getTitle()); - chooser.setApproveButtonText(getTitle()); - chooser.setFileSelectionMode(getSelectionMode()); - // No way for script to specify filter.... - - return chooser; - } - } - - /** - * Compared to {@link FileChooserEditor}, this does not require the user to enter a full path. - * Nor will it resolve file names against the working directory. It's just a text box with a - * file browser assist. - */ - public static class PathEditor extends PropertyEditorSupport { - private final FileChooserPanel panel = newChooserPanel(); - - protected FileChooserPanel newChooserPanel() { - return new FileChooserPanel(this::firePropertyChange); - } - - @Override - public String getAsText() { - return panel.textField.getText().trim(); - } - - @Override - public Object getValue() { - String text = panel.textField.getText().trim(); - if (text.isEmpty()) { - return null; - } - return Paths.get(text); - } - - @Override - public void setAsText(String text) throws IllegalArgumentException { - if (text == null || text.isBlank()) { - panel.textField.setText(""); - } - else { - panel.textField.setText(text); - } - } - - @Override - public void setValue(Object value) { - if (value == null) { - panel.textField.setText(""); - } - else if (value instanceof String s) { - panel.textField.setText(s); - } - else if (value instanceof Path p) { - panel.textField.setText(p.toString()); - } - else { - throw new IllegalArgumentException("value=" + value); - } - } - - @Override - public boolean supportsCustomEditor() { - return true; - } - - @Override - public Component getCustomEditor() { - return panel; - } - } - - public static class PathIsDirEditor extends PathEditor { - @Override - protected FileChooserPanel newChooserPanel() { - return new FileChooserPanel(this::firePropertyChange) { - @Override - protected String getTitle() { - return "Choose Directory"; - } - - @Override - protected GhidraFileChooserMode getSelectionMode() { - return GhidraFileChooserMode.DIRECTORIES_ONLY; - } - }; - } - - @Override - public Object getValue() { - Object value = super.getValue(); - if (value == null) { - return null; - } - if (value instanceof Path p) { - return new PathIsDir(p); - } - throw new AssertionError(); - } - - @Override - public void setValue(Object value) { - if (value instanceof PathIsDir dir) { - super.setValue(dir.path()); - } - else { - super.setValue(value); - } - } - } - - public static class PathIsFileEditor extends PathEditor { - @Override - protected FileChooserPanel newChooserPanel() { - return new FileChooserPanel(this::firePropertyChange) { - @Override - protected String getTitle() { - return "Choose File"; - } - - @Override - protected GhidraFileChooserMode getSelectionMode() { - return GhidraFileChooserMode.FILES_ONLY; - } - }; - } - - @Override - public Object getValue() { - Object value = super.getValue(); - if (value == null) { - return null; - } - if (value instanceof Path p) { - return new PathIsFile(p); - } - throw new AssertionError(); - } - - @Override - public void setValue(Object value) { - if (value instanceof PathIsFile file) { - super.setValue(file.path()); - } - else { - super.setValue(value); - } - } - } - - static { - PropertyEditorManager.registerEditor(BigInteger.class, BigIntEditor.class); - PropertyEditorManager.registerEditor(Path.class, PathEditor.class); - PropertyEditorManager.registerEditor(PathIsDir.class, PathIsDirEditor.class); - PropertyEditorManager.registerEditor(PathIsFile.class, PathIsFileEditor.class); - } - - private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments"; - - static class ChoicesPropertyEditor implements PropertyEditor { - private final List choices; - private final String[] tags; - - private final List listeners = new ArrayList<>(); - - private Object value; - - public ChoicesPropertyEditor(Set choices) { - this.choices = List.copyOf(choices); - this.tags = choices.stream().map(Objects::toString).toArray(String[]::new); - } - - @Override - public void setValue(Object value) { - if (Objects.equals(value, this.value)) { - return; - } - if (!choices.contains(value)) { - throw new IllegalArgumentException("Unsupported value: " + value); - } - Object oldValue; - List listeners; - synchronized (this.listeners) { - oldValue = this.value; - this.value = value; - if (this.listeners.isEmpty()) { - return; - } - listeners = List.copyOf(this.listeners); - } - PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value); - for (PropertyChangeListener l : listeners) { - l.propertyChange(evt); - } - } - - @Override - public Object getValue() { - return value; - } - - @Override - public boolean isPaintable() { - return false; - } - - @Override - public void paintValue(Graphics gfx, Rectangle box) { - // Not paintable - } - - @Override - public String getJavaInitializationString() { - if (value == null) { - return "null"; - } - if (value instanceof String str) { - return "\"" + StringEscapeUtils.escapeJava(str) + "\""; - } - return Objects.toString(value); - } - - @Override - public String getAsText() { - return Objects.toString(value); - } - - @Override - public void setAsText(String text) throws IllegalArgumentException { - int index = ArrayUtils.indexOf(tags, text); - if (index < 0) { - throw new IllegalArgumentException("Unsupported value: " + text); - } - setValue(choices.get(index)); - } - - @Override - public String[] getTags() { - return tags.clone(); - } - - @Override - public Component getCustomEditor() { - return null; - } - - @Override - public boolean supportsCustomEditor() { - return false; - } - - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - synchronized (listeners) { - listeners.add(listener); - } - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - synchronized (listeners) { - listeners.remove(listener); - } - } - } - - final static class NameTypePair extends MutablePair> { - - public static NameTypePair fromParameter(ParameterDescription parameter) { - return new NameTypePair(parameter.name, parameter.type); - } - - public static NameTypePair fromString(String name) throws ClassNotFoundException { - String[] parts = name.split(",", 2); - if (parts.length != 2) { - // This appears to be a bad assumption - empty fields results in solitary labels - return new NameTypePair(parts[0], String.class); - //throw new IllegalArgumentException("Could not parse name,type"); - } - return new NameTypePair(parts[0], Class.forName(parts[1])); - } - - public NameTypePair(String name, Class type) { - super(name, type); - } - - @Override - public String toString() { - return getName() + "," + getType().getName(); - } - - @Override - public Class setValue(Class value) { - throw new UnsupportedOperationException(); - } - - public String getName() { - return getLeft(); - } - - public Class getType() { - return getRight(); - } - } - - private final BidiMap, PropertyEditor> paramEditors = - new DualLinkedHashBidiMap<>(); - - private JPanel panel; - private JLabel descriptionLabel; - private JPanel pairPanel; - private PairLayout layout; - - protected JButton invokeButton; - protected JButton resetButton; - protected boolean resetRequested; - - private final PluginTool tool; - Map> parameters; - - // TODO: Not sure this is the best keying, but I think it works. - private Map memorized = new HashMap<>(); - private Map arguments; +public class DebuggerMethodInvocationDialog + extends AbstractDebuggerParameterDialog> { public DebuggerMethodInvocationDialog(PluginTool tool, String title, String buttonText, Icon buttonIcon) { - super(title, true, true, true, false); - this.tool = tool; - - populateComponents(buttonText, buttonIcon); - setRememberSize(false); - } - - protected Object computeMemorizedValue(ParameterDescription parameter) { - return memorized.computeIfAbsent(NameTypePair.fromParameter(parameter), - ntp -> parameter.defaultValue); - } - - public Map promptArguments(Map> parameterMap) { - setParameters(parameterMap); - tool.showDialog(this); - - return getArguments(); - } - - public void setParameters(Map> parameterMap) { - this.parameters = parameterMap; - populateOptions(); - } - - private void populateComponents(String buttonText, Icon buttonIcon) { - panel = new JPanel(new BorderLayout()); - panel.setBorder(new EmptyBorder(10, 10, 10, 10)); - - layout = new PairLayout(5, 5); - pairPanel = new JPanel(layout); - - JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER)); - JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, - JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - //scrolling.setPreferredSize(new Dimension(100, 130)); - panel.add(scrolling, BorderLayout.CENTER); - centering.add(pairPanel); - - descriptionLabel = new JLabel(); - descriptionLabel.setMaximumSize(new Dimension(300, 100)); - panel.add(descriptionLabel, BorderLayout.NORTH); - - addWorkPanel(panel); - - invokeButton = new JButton(buttonText, buttonIcon); - addButton(invokeButton); - resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH); - addButton(resetButton); - addCancelButton(); - - invokeButton.addActionListener(this::invoke); - resetButton.addActionListener(this::reset); - resetRequested = false; + super(tool, title, buttonText, buttonIcon); } @Override - protected void cancelCallback() { - this.arguments = null; - this.resetRequested = false; - close(); - } - - void invoke(ActionEvent evt) { - this.arguments = TargetMethod.validateArguments(parameters, collectArguments(), false); - this.resetRequested = false; - close(); - } - - void reset(ActionEvent evt) { - this.arguments = new LinkedHashMap<>(); - this.resetRequested = true; - close(); - } - - protected PropertyEditor getEditor(ParameterDescription param) { - if (!param.choices.isEmpty()) { - return new ChoicesPropertyEditor(param.choices); - } - Class type = param.type; - PropertyEditor editor = PropertyEditorManager.findEditor(type); - if (editor != null) { - return editor; - } - Msg.warn(this, "No editor for " + type + "? Trying String instead"); - return PropertyEditorManager.findEditor(String.class); - } - - void populateOptions() { - pairPanel.removeAll(); - paramEditors.clear(); - for (ParameterDescription param : parameters.values()) { - JLabel label = new JLabel(param.display); - label.setToolTipText(param.description); - pairPanel.add(label); - - PropertyEditor editor = getEditor(param); - Object val = computeMemorizedValue(param); - if (val == null) { - editor.setValue(""); - } - else { - editor.setValue(val); - } - editor.addPropertyChangeListener(this); - pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); - paramEditors.put(param, editor); - } - } - - protected Map collectArguments() { - Map map = new LinkedHashMap<>(); - for (ParameterDescription param : paramEditors.keySet()) { - Object val = memorized.get(NameTypePair.fromParameter(param)); - if (val != null) { - map.put(param.name, val); - } - } - return map; - } - - public Map getArguments() { - return arguments; - } - - public void setMemorizedArgument(String name, Class type, T value) { - if (value == null) { - return; - } - memorized.put(new NameTypePair(name, type), value); - } - - public T getMemorizedArgument(String name, Class type) { - return type.cast(memorized.get(new NameTypePair(name, type))); - } - - public void forgetMemorizedArguments() { - memorized.clear(); + protected String parameterName(ParameterDescription parameter) { + return parameter.name; } @Override - public void propertyChange(PropertyChangeEvent evt) { - PropertyEditor editor = (PropertyEditor) evt.getSource(); - ParameterDescription param = paramEditors.getKey(editor); - memorized.put(NameTypePair.fromParameter(param), editor.getValue()); + protected Class parameterType(ParameterDescription parameter) { + return parameter.type; } - public void writeConfigState(SaveState saveState) { - SaveState subState = new SaveState(); - for (Map.Entry ent : memorized.entrySet()) { - NameTypePair ntp = ent.getKey(); - ConfigStateField.putState(subState, ntp.getType().asSubclass(Object.class), - ntp.getName(), ent.getValue()); - } - saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml()); + @Override + protected String parameterLabel(ParameterDescription parameter) { + return parameter.display; } - public void readConfigState(SaveState saveState) { - Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS); - if (element == null) { - return; - } - SaveState subState = new SaveState(element); - for (String name : subState.getNames()) { - try { - NameTypePair ntp = NameTypePair.fromString(name); - memorized.put(ntp, - ConfigStateField.getState(subState, ntp.getType(), ntp.getName())); - } - catch (Exception e) { - Msg.error(this, "Error restoring memorized parameter " + name, e); - } - } + @Override + protected String parameterToolTip(ParameterDescription parameter) { + return parameter.description; } - public boolean isResetRequested() { - return resetRequested; + @Override + protected ValStr parameterDefault(ParameterDescription parameter) { + return ValStr.from(parameter.defaultValue); } - public void setDescription(String htmlDescription) { - if (htmlDescription == null) { - descriptionLabel.setBorder(BorderFactory.createEmptyBorder()); - descriptionLabel.setText(""); - } - else { - descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - descriptionLabel.setText(htmlDescription); - } + @Override + protected Set parameterChoices(ParameterDescription parameter) { + return parameter.choices; + } + + @Override + protected Map> validateArguments( + Map> parameters, Map> arguments) { + Map args = ValStr.toPlainMap(arguments); + return ValStr.fromPlainMap(TargetMethod.validateArguments(parameters, args, false)); + } + + @Override + protected void parameterSaveValue(ParameterDescription parameter, SaveState state, + String key, ValStr value) { + ConfigStateField.putState(state, parameter.type.asSubclass(Object.class), key, value.val()); + } + + @Override + protected ValStr parameterLoadValue(ParameterDescription parameter, SaveState state, + String key) { + return ValStr.from(ConfigStateField.getState(state, parameter.type, key)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java index 0cc8aeaf44..5e4a766a83 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,10 +16,12 @@ package ghidra.app.plugin.core.debug.service.model; import java.util.*; +import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import docking.ActionContext; import ghidra.app.context.ProgramLocationActionContext; @@ -38,6 +40,7 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.util.PathMatcher; import ghidra.dbg.util.PathPredicates; +import ghidra.debug.api.ValStr; import ghidra.debug.api.model.*; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.tracemgr.DebuggerCoordinates; @@ -237,26 +240,12 @@ public class TraceRecorderTarget extends AbstractTarget { } private Map promptArgs(TargetMethod method, Map defaults) { + Map> defs = ValStr.fromPlainMap(defaults); DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool, method.getDisplay(), method.getDisplay(), null); - while (true) { - for (ParameterDescription param : method.getParameters().values()) { - Object val = defaults.get(param.name); - if (val != null) { - dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class), - val); - } - } - Map args = dialog.promptArguments(method.getParameters()); - if (args == null) { - // Cancelled - return null; - } - if (dialog.isResetRequested()) { - continue; - } - return args; - } + + Map> args = dialog.promptArguments(method.getParameters(), defs, defs); + return args == null ? null : ValStr.toPlainMap(args); } private CompletableFuture invokeMethod(boolean prompt, TargetMethod method, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java index f9000c5268..0295f0abcc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.service.model.launch; -import static ghidra.async.AsyncUtils.*; +import static ghidra.async.AsyncUtils.loop; import java.io.File; import java.io.IOException; @@ -44,6 +44,7 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; +import ghidra.debug.api.ValStr; import ghidra.debug.api.model.DebuggerProgramLaunchOffer; import ghidra.debug.api.model.TraceRecorder; import ghidra.debug.api.modules.*; @@ -281,14 +282,14 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg return proposal; } - private void saveLauncherArgs(Map args, + private void saveLauncherArgs(Map> args, Map> params) { SaveState state = new SaveState(); for (ParameterDescription param : params.values()) { - Object val = args.get(param.name); + ValStr val = args.get(param.name); if (val != null) { ConfigStateField.putState(state, param.type.asSubclass(Object.class), param.name, - val); + val.val()); } } if (program != null) { @@ -316,19 +317,19 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param params the parameters * @return the default arguments */ - protected Map generateDefaultLauncherArgs( + protected Map> generateDefaultLauncherArgs( Map> params) { if (program == null) { return Map.of(); } - Map map = new LinkedHashMap(); + Map> map = new LinkedHashMap<>(); for (Entry> entry : params.entrySet()) { - map.put(entry.getKey(), entry.getValue().defaultValue); + map.put(entry.getKey(), ValStr.from(entry.getValue().defaultValue)); } String almostExecutablePath = program.getExecutablePath(); File f = new File(almostExecutablePath); map.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, - TargetCmdLineLauncher.quoteImagePathIfSpaces(f.getAbsolutePath())); + ValStr.from(TargetCmdLineLauncher.quoteImagePathIfSpaces(f.getAbsolutePath()))); return map; } @@ -338,36 +339,19 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param params the parameters of the model's launcher * @return the arguments given by the user, or null if cancelled */ - protected Map promptLauncherArgs(TargetLauncher launcher, + protected Map> promptLauncherArgs(TargetLauncher launcher, LaunchConfigurator configurator) { TargetParameterMap params = launcher.getParameters(); DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool, getButtonTitle(), "Launch", getIcon()); + // NB. Do not invoke read/writeConfigState - Map args; - boolean reset = false; - do { - args = configurator.configureLauncher(launcher, - loadLastLauncherArgs(launcher, true), RelPrompt.BEFORE); - for (ParameterDescription param : params.values()) { - Object val = args.get(param.name); - if (val != null) { - dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class), - val); - } - } - args = dialog.promptArguments(params); - if (args == null) { - // Cancelled - return null; - } - reset = dialog.isResetRequested(); - if (reset) { - args = generateDefaultLauncherArgs(params); - } - saveLauncherArgs(args, params); - } - while (reset); + + Map> defaultArgs = generateDefaultLauncherArgs(params); + Map> lastArgs = configurator.configureLauncher(launcher, + loadLastLauncherArgs(launcher, true), RelPrompt.BEFORE); + Map> args = dialog.promptArguments(params, lastArgs, defaultArgs); + saveLauncherArgs(args, params); return args; } @@ -386,7 +370,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param forPrompt true if the user will be confirming the arguments * @return the loaded arguments, or defaults */ - protected Map loadLastLauncherArgs(TargetLauncher launcher, boolean forPrompt) { + protected Map> loadLastLauncherArgs(TargetLauncher launcher, + boolean forPrompt) { /** * TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers. * Re-examine this if/when that gets merged @@ -401,13 +386,13 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg Element element = XmlUtilities.fromString(property); SaveState state = new SaveState(element); List names = List.of(state.getNames()); - Map args = new LinkedHashMap<>(); + Map> args = new LinkedHashMap<>(); for (ParameterDescription param : params.values()) { if (names.contains(param.name)) { Object configState = ConfigStateField.getState(state, param.type, param.name); if (configState != null) { - args.put(param.name, configState); + args.put(param.name, ValStr.from(configState)); } } } @@ -426,7 +411,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg e); } } - Map args = generateDefaultLauncherArgs(params); + Map> args = generateDefaultLauncherArgs(params); saveLauncherArgs(args, params); return args; } @@ -447,7 +432,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg * @param configurator a means of configuring the launcher * @return the chosen arguments, or null if the user cancels at the prompt */ - public Map getLauncherArgs(TargetLauncher launcher, boolean prompt, + public Map> getLauncherArgs(TargetLauncher launcher, boolean prompt, LaunchConfigurator configurator) { return prompt ? configurator.configureLauncher(launcher, @@ -456,7 +441,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg RelPrompt.NONE); } - public Map getLauncherArgs(TargetLauncher launcher, boolean prompt) { + public Map> getLauncherArgs(TargetLauncher launcher, boolean prompt) { return getLauncherArgs(launcher, prompt, LaunchConfigurator.NOP); } @@ -541,13 +526,14 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg // Eww. protected CompletableFuture launch(TargetLauncher launcher, boolean prompt, LaunchConfigurator configurator, TaskMonitor monitor) { - Map args = getLauncherArgs(launcher, prompt, configurator); + Map> args = getLauncherArgs(launcher, prompt, configurator); if (args == null) { throw new CancellationException(); } + Map a = ValStr.toPlainMap(args); return AsyncTimer.DEFAULT_TIMER.mark() .timeOut( - launcher.launch(args), getTimeoutMillis(), () -> onTimedOutLaunch(monitor)); + launcher.launch(a), getTimeoutMillis(), () -> onTimedOutLaunch(monitor)); } protected void checkCancelled(TaskMonitor monitor) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/MiscellaneousUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/MiscellaneousUtils.java index 82b159ab51..f54933bee4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/MiscellaneousUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/MiscellaneousUtils.java @@ -33,8 +33,9 @@ public enum MiscellaneousUtils { * Obtain a swing component which may be used to edit the property. * *

- * This has been shamelessly stolen from {@link EditorState#getEditorComponent()}, which seems - * entangled with Ghidra's whole options system. I think this portion could be factored out. + * This has was originally stolen from {@link EditorState#getEditorComponent()}, which seems + * entangled with Ghidra's whole options system. Can that be factored out? Since then, the two + * have drifted apart. * * @param editor the editor for which to obtain an interactive component for editing * @return the component @@ -53,16 +54,11 @@ public enum MiscellaneousUtils { return new PropertyText(editor); } - Class clazz = editor.getClass(); - String clazzName = clazz.getSimpleName(); - if (clazzName.startsWith("String")) { - // Most likely some kind of string editor with a null value. Just use a string - // property and let the value be empty. - return new PropertyText(editor); - } - - throw new IllegalStateException( - "Ghidra does not know how to use PropertyEditor: " + editor.getClass().getName()); + /** + * TODO: Would be nice to know the actual type, but alas! Just default to a PropertyText and + * hope all goes well. + */ + return new PropertyText(editor); } public static void rigFocusAndEnter(Component c, Runnable runnable) { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/InvocationDialogHelper.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/InvocationDialogHelper.java new file mode 100644 index 0000000000..b65286bbbe --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/InvocationDialogHelper.java @@ -0,0 +1,94 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui; + +import java.beans.PropertyEditor; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import docking.test.AbstractDockingTest; +import ghidra.async.SwingExecutorService; +import ghidra.debug.api.ValStr; +import ghidra.framework.options.SaveState; + +public class InvocationDialogHelper> { + + public static > InvocationDialogHelper waitFor( + Class cls) { + D dialog = AbstractDockingTest.waitForDialogComponent(cls); + return new InvocationDialogHelper<>(dialog); + } + + private final AbstractDebuggerParameterDialog

dialog; + + public InvocationDialogHelper(AbstractDebuggerParameterDialog

dialog) { + this.dialog = dialog; + } + + public void dismissWithArguments(Map> args) { + dialog.setMemorizedArguments(args); + invoke(); + } + + public Map.Entry> entry(String key, T value) { + return Map.entry(key, ValStr.from(value)); + } + + public void setArg(P param, Object value) { + PropertyEditor editor = dialog.getEditor(param); + runSwing(() -> editor.setValue(value)); + } + + protected void runSwing(Runnable r) { + try { + CompletableFuture.runAsync(r, SwingExecutorService.LATER).get(); + } + catch (ExecutionException e) { + switch (e.getCause()) { + case RuntimeException t -> throw t; + case Exception t -> throw new RuntimeException(t); + default -> ExceptionUtils.rethrow(e.getCause()); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void setArgAsString(P param, String value) { + PropertyEditor editor = dialog.getEditor(param); + runSwing(() -> editor.setAsText(value)); + } + + public void invoke() { + runSwing(() -> dialog.invoke(null)); + } + + public SaveState saveState() { + SaveState parent = new SaveState(); + runSwing(() -> dialog.writeConfigState(parent)); + return parent.getSaveState(AbstractDebuggerParameterDialog.KEY_MEMORIZED_ARGUMENTS); + } + + public void loadState(SaveState state) { + SaveState parent = new SaveState(); + parent.putSaveState(AbstractDebuggerParameterDialog.KEY_MEMORIZED_ARGUMENTS, state); + runSwing(() -> dialog.readConfigState(parent)); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java deleted file mode 100644 index 1b1c43af88..0000000000 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java +++ /dev/null @@ -1,48 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.debug.gui.objects.components; - -import static org.junit.Assert.assertNotNull; - -import java.util.Map; - -import docking.test.AbstractDockingTest; -import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.util.Swing; - -public class InvocationDialogHelper { - - public static InvocationDialogHelper waitFor() { - DebuggerMethodInvocationDialog dialog = - AbstractDockingTest.waitForDialogComponent(DebuggerMethodInvocationDialog.class); - return new InvocationDialogHelper(dialog); - } - - private final DebuggerMethodInvocationDialog dialog; - - public InvocationDialogHelper(DebuggerMethodInvocationDialog dialog) { - this.dialog = dialog; - } - - public void dismissWithArguments(Map args) { - for (Map.Entry a : args.entrySet()) { - ParameterDescription p = dialog.parameters.get(a.getKey()); - assertNotNull(p); - dialog.setMemorizedArgument(a.getKey(), p.type.asSubclass(Object.class), a.getValue()); - } - Swing.runNow(() -> dialog.invoke(null)); - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/NumericUtilities.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/NumericUtilities.java index beafa27700..a7e3ec1a23 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/NumericUtilities.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/NumericUtilities.java @@ -33,6 +33,8 @@ public final class NumericUtilities { private final static String HEX_PREFIX_X = "0X"; private final static String HEX_PREFIX_x = "0x"; + private final static String BIN_PREFIX = "0B"; + private final static String OCT_PREFIX = "0"; private final static Set> INTEGER_TYPES = new HashSet<>(); static { @@ -235,6 +237,49 @@ public final class NumericUtilities { return new BigInteger(s, 16); } + private static BigInteger decodeMagnitude(int p, String s) { + // Special case, so it doesn't get chewed by octal parser + if ("0".equals(s)) { + return BigInteger.ZERO; + } + if (s.regionMatches(true, p, HEX_PREFIX_X, 0, HEX_PREFIX_X.length())) { + return new BigInteger(s.substring(p + HEX_PREFIX_X.length()), 16); + } + if (s.regionMatches(true, p, BIN_PREFIX, 0, BIN_PREFIX.length())) { + return new BigInteger(s.substring(p + BIN_PREFIX.length()), 2); + } + // Check last, because prefix is shortest. + if (s.regionMatches(true, p, OCT_PREFIX, 0, OCT_PREFIX.length())) { + return new BigInteger(s.substring(p + OCT_PREFIX.length()), 8); + } + return new BigInteger(s.substring(p), 10); + } + + /** + * Decode a big integer in hex, binary, octal, or decimal, based on the prefix 0x, 0b, or 0. + * + *

+ * This checks for the presence of a case-insensitive prefix. 0x denotes hex, 0b denotes binary, + * 0 denotes octal. If no prefix is given, decimal is assumed. A sign +/- may immediately + * precede the prefix. If no sign is given, a positive value is assumed. + * + * @param s the string to parse + * @return the decoded value + */ + public static BigInteger decodeBigInteger(String s) { + int p = 0; + boolean negative = false; + if (s.startsWith("+")) { + p = 1; + } + else if (s.startsWith("-")) { + p = 1; + negative = true; + } + BigInteger mag = decodeMagnitude(p, s); + return negative ? mag.negate() : mag; + } + /** * returns the value of the specified long as hexadecimal, prefixing with the * {@link #HEX_PREFIX_x} string. diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/NumericUtilitiesTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/NumericUtilitiesTest.java index cc63d8d81e..b8c4731ed5 100644 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/NumericUtilitiesTest.java +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/NumericUtilitiesTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,6 +18,7 @@ package ghidra.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import java.math.BigInteger; import java.util.*; import java.util.stream.Collectors; @@ -332,4 +333,89 @@ public class NumericUtilitiesTest { assertEquals(errorMessage, expected[i], actual[i]); } } + + @Test + public void testDecodeBigInteger() { + // Zero special cases + assertEquals(BigInteger.ZERO, NumericUtilities.decodeBigInteger("0")); + assertEquals(BigInteger.ZERO, NumericUtilities.decodeBigInteger("000")); + // Decimal + assertEquals(BigInteger.valueOf(99), NumericUtilities.decodeBigInteger("99")); + assertEquals(BigInteger.valueOf(99), NumericUtilities.decodeBigInteger("+99")); + assertEquals(BigInteger.valueOf(-99), NumericUtilities.decodeBigInteger("-99")); + // Hex + assertEquals(BigInteger.valueOf(0x99), NumericUtilities.decodeBigInteger("0x99")); + assertEquals(BigInteger.valueOf(0x99), NumericUtilities.decodeBigInteger("+0x99")); + assertEquals(BigInteger.valueOf(-0x99), NumericUtilities.decodeBigInteger("-0x99")); + // Binary + assertEquals(BigInteger.valueOf(0b110), NumericUtilities.decodeBigInteger("0b110")); + assertEquals(BigInteger.valueOf(0b110), NumericUtilities.decodeBigInteger("+0b110")); + assertEquals(BigInteger.valueOf(-0b110), NumericUtilities.decodeBigInteger("-0b110")); + // Octal + assertEquals(BigInteger.valueOf(0755), NumericUtilities.decodeBigInteger("0755")); + assertEquals(BigInteger.valueOf(0755), NumericUtilities.decodeBigInteger("+0755")); + assertEquals(BigInteger.valueOf(-0755), NumericUtilities.decodeBigInteger("-0755")); + + // Errors + try { + NumericUtilities.decodeBigInteger(""); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("+"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("-"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("0x"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("0b"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("a01"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("081"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("0x9g"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger(" 10"); + fail(); + } + catch (NumberFormatException e) { + } + try { + NumericUtilities.decodeBigInteger("10 "); + fail(); + } + catch (NumberFormatException e) { + } + } } diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/screen/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginScreenShots.java b/Ghidra/Test/DebuggerIntegrationTest/src/screen/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginScreenShots.java index 22f8a7be08..5146eac664 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/screen/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginScreenShots.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/screen/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginScreenShots.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import org.junit.Test; import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; import ghidra.app.plugin.core.terminal.TerminalProvider; +import ghidra.debug.api.ValStr; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.framework.plugintool.AutoConfigState.PathIsFile; import ghidra.test.ToyProgramBuilder; @@ -30,7 +31,8 @@ import help.screenshot.GhidraScreenShotGenerator; public class TraceRmiLauncherServicePluginScreenShots extends GhidraScreenShotGenerator { TraceRmiLauncherServicePlugin servicePlugin; - protected void captureLauncherByTitle(String title, Map args) throws Throwable { + protected void captureLauncherByTitle(String title, Map> args) + throws Throwable { servicePlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class); ToyProgramBuilder pb = new ToyProgramBuilder("demo", false); @@ -49,10 +51,13 @@ public class TraceRmiLauncherServicePluginScreenShots extends GhidraScreenShotGe captureDialog(DebuggerMethodInvocationDialog.class); } + protected ValStr fileArg(String path) { + return new ValStr<>(new PathIsFile(Paths.get(path)), path); + } + @Test public void testCaptureGdbLauncher() throws Throwable { - captureLauncherByTitle("gdb", - Map.of("arg:1", new PathIsFile(Paths.get("/home/user/demo")))); + captureLauncherByTitle("gdb", Map.of("arg:1", fileArg("/home/user/demo"))); } @Test diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptTraceRmiLaunchOfferTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptTraceRmiLaunchOfferTest.java new file mode 100644 index 0000000000..3f0b7a3229 --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptTraceRmiLaunchOfferTest.java @@ -0,0 +1,584 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import static org.junit.Assert.*; + +import java.io.*; +import java.math.BigInteger; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.AbstractTraceRmiLaunchOffer.NullPtyTerminalSession; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.AbstractTraceRmiLaunchOffer.PtyTerminalSession; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ParseException; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.app.plugin.core.debug.service.target.DebuggerTargetServicePlugin; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler; +import ghidra.app.plugin.core.terminal.TerminalListener; +import ghidra.app.services.Terminal; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.TerminalSession; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.program.model.listing.Program; +import ghidra.pty.*; +import ghidra.rmi.trace.TraceRmi.*; +import ghidra.util.Msg; +import ghidra.util.task.ConsoleTaskMonitor; + +public class ScriptTraceRmiLaunchOfferTest extends AbstractGhidraHeadedDebuggerTest { + + static class TestScriptAttributesParser extends ScriptAttributesParser { + List errors = new ArrayList<>(); + + @Override + protected boolean ignoreLine(int lineNo, String line) { + return false; + } + + @Override + protected String removeDelimiter(String line) { + return line; + } + + @Override + protected void reportError(String message) { + super.reportError(message); + errors.add(message); + } + } + + static ScriptAttributes parse(String header, String name) throws ParseException { + try { + TestScriptAttributesParser parser = new TestScriptAttributesParser(); + ScriptAttributes attributes = + parser.parseStream(new ByteArrayInputStream(header.getBytes()), name); + if (!parser.errors.isEmpty()) { + throw new ParseException(null, parser.errors.toString()); + } + return attributes; + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + record Config(Map> args) implements LaunchConfigurator { + public static final Config DEFAULTS = new Config(Map.of()); + + @Override + public PromptMode getPromptMode() { + return PromptMode.NEVER; + } + + @Override + public Map> configureLauncher(TraceRmiLaunchOffer offer, + Map> arguments, RelPrompt relPrompt) { + Map> mod = new HashMap<>(arguments); + mod.putAll(args); + return mod; + } + } + + record MockTerminal() implements Terminal { + @Override + public void addTerminalListener(TerminalListener listener) { + } + + @Override + public void removeTerminalListener(TerminalListener listener) { + } + + @Override + public void injectDisplayOutput(ByteBuffer bb) { + } + + @Override + public void setSubTitle(String title) { + } + + @Override + public String getSubTitle() { + return null; + } + + @Override + public void setFixedSize(short cols, short rows) { + } + + @Override + public void setDynamicSize() { + } + + @Override + public void setMaxScrollBackRows(int rows) { + } + + @Override + public int getColumns() { + return 0; + } + + @Override + public int getRows() { + return 0; + } + + @Override + public int getScrollBackRows() { + return 0; + } + + @Override + public String getFullText() { + return null; + } + + @Override + public String getDisplayText() { + return null; + } + + @Override + public String getLineText(int line) { + return null; + } + + @Override + public String getRangeText(int startCol, int startLine, int endCol, int endLine) { + return null; + } + + @Override + public int getCursorRow() { + return 0; + } + + @Override + public int getCursorColumn() { + return 0; + } + + @Override + public void close() { + } + + @Override + public void terminated() { + } + + @Override + public void setTerminateAction(Runnable action) { + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public void toFront() { + } + } + + record MockPtySession() implements PtySession { + @Override + public int waitExited() throws InterruptedException { + return 0; + } + + @Override + public int waitExited(long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException { + return 0; + } + + @Override + public void destroyForcibly() { + } + + @Override + public String description() { + return null; + } + } + + record MockPty() implements Pty { + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public PtyParent getParent() { + return null; + } + + @Override + public PtyChild getChild() { + return null; + } + + @Override + public void close() throws IOException { + } + } + + File nameTempFile() { + return Paths.get(getTestDirectoryPath(), name.getMethodName()).toFile(); + } + + static class MockClient extends Thread implements AutoCloseable { + private static final DomObjId TRACE_ID = DomObjId.newBuilder().setId(0).build(); + private final SocketAddress addr; + private final String name; + + private Throwable exc; + + Socket s; + OutputStream out; + InputStream in; + + public MockClient(SocketAddress addr, String name) { + setDaemon(true); + this.addr = addr; + this.name = name; + } + + void send(RootMessage msg) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + buf.putInt(msg.getSerializedSize()); + out.write(buf.array()); + msg.writeTo(out); + out.flush(); + } + + RootMessage recv() throws IOException { + int len = ByteBuffer.wrap(in.readNBytes(Integer.BYTES)).getInt(); + return RootMessage.parseFrom(in.readNBytes(len)); + } + + void completeNegotiation() throws IOException { + send(RootMessage.newBuilder() + .setRequestNegotiate(RequestNegotiate.newBuilder() + .setVersion(TraceRmiHandler.VERSION) + .setDescription("Mock Client")) + .build()); + Msg.debug(this, "Sent negotation request"); + RootMessage reply = recv(); + Msg.debug(this, "Received: " + reply); + assertNotNull(reply.getReplyNegotiate()); + } + + void createTrace() throws IOException { + send(RootMessage.newBuilder() + .setRequestCreateTrace(RequestCreateTrace.newBuilder() + .setOid(TRACE_ID) + .setLanguage(Language.newBuilder().setId("Toy:BE:64:default")) + .setCompiler(Compiler.newBuilder().setId("default")) + .setPath(FilePath.newBuilder().setPath(name))) + .build()); + Msg.debug(this, "Sent createTrace request"); + RootMessage reply = recv(); + Msg.debug(this, "Received: " + reply); + assertNotNull(reply.getReplyCreateTrace()); + } + + protected void doRun() throws Throwable { + s = new Socket(); + s.connect(addr); + out = s.getOutputStream(); + in = s.getInputStream(); + + completeNegotiation(); + + createTrace(); + + s.close(); + } + + @Override + public void run() { + try { + doRun(); + } + catch (Throwable e) { + Msg.error(this, "Mock client crashed", e); + this.exc = e; + } + } + + @Override + public void close() throws Exception { + join(1000); + if (exc != null) { + throw new RuntimeException("Exception in mock client", exc); + } + assertFalse(isAlive()); + } + } + + class TestScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer { + int nextNullId = 0; + + public TestScriptTraceRmiLaunchOffer(Program program, String header) throws ParseException { + super(launchPlugin, program, nameTempFile(), name.getMethodName(), + parse(header, name.getMethodName())); + } + + @Override + protected NullPtyTerminalSession nullPtyTerminal() throws IOException { + return new NullPtyTerminalSession(new MockTerminal(), new MockPty(), + "null-" + (++nextNullId)); + } + + @Override + protected PtyTerminalSession runInTerminal(List commandLine, + Map env, File workingDirectory, + Collection subordinates) throws IOException { + String host = env.get(ScriptAttributesParser.ENV_GHIDRA_TRACE_RMI_HOST); + int port = Integer.parseInt(env.get(ScriptAttributesParser.ENV_GHIDRA_TRACE_RMI_PORT)); + // The plugin is waiting for a connection. Have to satisfy it to move on. + client = new MockClient(new InetSocketAddress(host, port), name.getMethodName()); + client.start(); + return new PtyTerminalSession(new MockTerminal(), new MockPty(), new MockPtySession(), + client); + } + } + + TraceRmiLauncherServicePlugin launchPlugin; + + MockClient client; + + record ResultAndClient(LaunchResult result, MockClient client) implements AutoCloseable { + @Override + public void close() throws Exception { + client.close(); + result.close(); + } + + public PtyTerminalSession mockSession() { + return new PtyTerminalSession(new MockTerminal(), new MockPty(), new MockPtySession(), + client); + } + + public NullPtyTerminalSession mockNull(String name) { + return new NullPtyTerminalSession(new MockTerminal(), new MockPty(), name); + } + } + + ResultAndClient launchNoErr(TraceRmiLaunchOffer offer, Map> args) + throws Throwable { + LaunchResult result = offer.launchProgram(new ConsoleTaskMonitor(), new Config(args)); + if (result.exception() != null) { + throw (result.exception()); + } + return new ResultAndClient(result, client); + } + + ResultAndClient launchNoErr(TraceRmiLaunchOffer offer) throws Throwable { + return launchNoErr(offer, Map.of()); + } + + @Before + public void setupOfferTest() throws PluginException { + // BUG: Seems I shouldn't have to do this. It's in servicesRequired (transitive) + addPlugin(tool, DebuggerTargetServicePlugin.class); + launchPlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class); + } + + @Test + public void testTitleOnly() throws Throwable { + createProgram(); + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(program, """ + @title Test + """); + + try (ResultAndClient rac = launchNoErr(offer)) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession())), + rac.result.sessions()); + } + } + + @Test + public void testTtyAlways() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @tty TTY_TARGET + """); + try (ResultAndClient rac = launchNoErr(offer)) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession()), + Map.entry("TTY_TARGET", rac.mockNull("null-1"))), + rac.result.sessions()); + } + } + + @Test + public void testTtyCondBoolFalse() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:bool=false "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY + """); + try (ResultAndClient rac = launchNoErr(offer)) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession())), + rac.result.sessions()); + } + } + + @Test + public void testTtyCondBoolTrue() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:bool=false "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY + """); + try (ResultAndClient rac = launchNoErr(offer, Map.of( + "env:OPT_EXTRA_TTY", ValStr.from(true)))) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession()), + Map.entry("TTY_TARGET", rac.mockNull("null-1"))), + rac.result.sessions()); + } + } + + @Test(expected = ParseException.class) + public void testTtyCondBoolTypeMismatch() throws Throwable { + new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_SOME_INT:int=0 "An integer" "Just an option for testing." + @tty TTY_TARGET if env:OPT_SOME_INT + """); + } + + @Test(expected = ParseException.class) + public void testTtyCondBoolNoSuch() throws Throwable { + new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @tty TTY_TARGET if env:NO_SUCH + """); + } + + @Test + public void testTtyCondStrEqFalse() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:str="No" "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY == "Yes" + """); + try (ResultAndClient rac = launchNoErr(offer)) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession())), + rac.result.sessions()); + } + } + + @Test + public void testTtyCondStrEqTrue() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:str="No" "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY == "Yes" + """); + try (ResultAndClient rac = launchNoErr(offer, Map.of( + "env:OPT_EXTRA_TTY", ValStr.str("Yes")))) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession()), + Map.entry("TTY_TARGET", rac.mockNull("null-1"))), + rac.result.sessions()); + } + } + + @Test(expected = ParseException.class) + public void testTtyCondStrEqNoSuch() throws Throwable { + new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @tty TTY_TARGET if env:NO_SUCH == "Yes" + """); + } + + @Test + public void testTtyCondIntEqFalse() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:int=0 "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY == 6 + """); + try (ResultAndClient rac = launchNoErr(offer)) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession())), + rac.result.sessions()); + } + } + + @Test + public void testTtyCondIntEqTrue() throws Throwable { + TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_EXTRA_TTY:int=0 "Extra TTY" "Provide a separate tty." + @tty TTY_TARGET if env:OPT_EXTRA_TTY == 0b110 + """); + try (ResultAndClient rac = launchNoErr(offer, Map.of( + "env:OPT_EXTRA_TTY", ValStr.from(BigInteger.valueOf(6))))) { + assertEquals(Map.ofEntries( + Map.entry("Shell", rac.mockSession()), + Map.entry("TTY_TARGET", rac.mockNull("null-1"))), + rac.result.sessions()); + } + } + + @Test(expected = ParseException.class) + public void testTtyCondIntEqParseErr() throws Throwable { + new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @env OPT_SOME_INT:int=0 "An integer" "Just an option for testing." + @tty TTY_TARGET if env:OPT_SOME_INT == "Yes" + """); + } + + @Test(expected = ParseException.class) + public void testTtyCondIntEqNoSuch() throws Throwable { + new TestScriptTraceRmiLaunchOffer(null, """ + @title Test + @no-image + @tty TTY_TARGET if env:NO_SUCH == 6 + """); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java index c467376c51..51e753a693 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,9 +20,8 @@ import static org.junit.Assert.assertEquals; import java.net.SocketAddress; import java.util.*; -import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.debug.api.tracermi.TerminalSession; -import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.*; import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; import ghidra.program.model.listing.Program; import ghidra.util.task.TaskMonitor; @@ -30,9 +29,9 @@ import ghidra.util.task.TaskMonitor; public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { public static class TestTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer { - private static final ParameterDescription PARAM_DESC_IMAGE = - ParameterDescription.create(String.class, "image", true, "", - PARAM_DISPLAY_IMAGE, "Image to execute"); + private static final LaunchParameter PARAM_IMAGE = + LaunchParameter.create(String.class, "image", PARAM_DISPLAY_IMAGE, "Image to execute", + true, ValStr.str(""), str -> str); public TestTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) { super(plugin, program); @@ -58,8 +57,8 @@ public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { } @Override - public Map> getParameters() { - return Map.ofEntries(Map.entry(PARAM_DESC_IMAGE.name, PARAM_DESC_IMAGE)); + public Map> getParameters() { + return LaunchParameter.mapOf(PARAM_IMAGE); } @Override @@ -69,19 +68,19 @@ public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { @Override protected void launchBackEnd(TaskMonitor monitor, Map sessions, - Map args, SocketAddress address) throws Exception { + Map> args, SocketAddress address) throws Exception { } @Override public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) { assertEquals(PromptMode.NEVER, configurator.getPromptMode()); - Map args = + Map> args = configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE); return new LaunchResult(program, null, null, null, null, - new RuntimeException("Test launcher cannot launch " + args.get("image"))); + new RuntimeException("Test launcher cannot launch " + PARAM_IMAGE.get(args).val())); } - public void saveLauncherArgs(Map args) { + public void saveLauncherArgs(Map> args) { super.saveLauncherArgs(args, getParameters()); } } diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialogTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialogTest.java new file mode 100644 index 0000000000..f44bbb2c4e --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLaunchDialogTest.java @@ -0,0 +1,255 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.math.BigInteger; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.*; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; +import ghidra.app.plugin.core.debug.gui.InvocationDialogHelper; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.BaseType; +import ghidra.async.SwingExecutorService; +import ghidra.debug.api.ValStr; +import ghidra.debug.api.tracermi.LaunchParameter; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.AutoConfigState.PathIsDir; +import ghidra.framework.plugintool.AutoConfigState.PathIsFile; + +public class TraceRmiLaunchDialogTest extends AbstractGhidraHeadedDebuggerTest { + private static final LaunchParameter PARAM_STRING = + BaseType.STRING.createParameter("some_string", "A String", "A string", + true, ValStr.str("Hello")); + private static final LaunchParameter PARAM_INT = + BaseType.INT.createParameter("some_int", "An Int", "An integer", + true, intVal(99)); + private static final LaunchParameter PARAM_BOOL = + BaseType.BOOL.createParameter("some_bool", "A Bool", "A boolean", + true, ValStr.from(true)); + private static final LaunchParameter PARAM_PATH = + BaseType.PATH.createParameter("some_path", "A Path", "A path", + true, pathVal("my_path")); + private static final LaunchParameter PARAM_DIR = + BaseType.DIR.createParameter("some_dir", "A Dir", "A directory", + true, dirVal("my_dir")); + private static final LaunchParameter PARAM_FILE = + BaseType.FILE.createParameter("some_file", "A File", "A file", + true, fileVal("my_file")); + + private TraceRmiLaunchDialog dialog; + + @Before + public void setupRmiLaunchDialogTest() throws Exception { + dialog = new TraceRmiLaunchDialog(tool, "Launch Test", "Launch", null); + } + + record PromptResult(CompletableFuture>> args, + InvocationDialogHelper, ?> h) {} + + protected PromptResult prompt(LaunchParameter... params) { + CompletableFuture>> args = CompletableFuture.supplyAsync( + () -> dialog.promptArguments(LaunchParameter.mapOf(params), Map.of(), Map.of()), + SwingExecutorService.LATER); + InvocationDialogHelper, ?> helper = + InvocationDialogHelper.waitFor(TraceRmiLaunchDialog.class); + return new PromptResult(args, helper); + } + + static ValStr intVal(long val, String str) { + return new ValStr<>(BigInteger.valueOf(val), str); + } + + static ValStr intVal(long val) { + return ValStr.from(BigInteger.valueOf(val)); + } + + static ValStr pathVal(String path) { + return new ValStr<>(Paths.get(path), path); + } + + static ValStr dirVal(String path) { + return new ValStr<>(PathIsDir.fromString(path), path); + } + + static ValStr fileVal(String path) { + return new ValStr<>(PathIsFile.fromString(path), path); + } + + @Test + public void testStringDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_STRING); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_string", ValStr.str("Hello")), args); + } + + @Test + public void testStringInputValue() throws Throwable { + PromptResult result = prompt(PARAM_STRING); + result.h.setArgAsString(PARAM_STRING, "World"); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_string", ValStr.str("World")), args); + } + + @Test + public void testIntDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_INT); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_int", intVal(99)), args); + } + + @Test + public void testIntInputHexValue() throws Throwable { + PromptResult result = prompt(PARAM_INT); + result.h.setArgAsString(PARAM_INT, "0x11"); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_int", intVal(17, "0x11")), args); + } + + @Test + public void testIntInputHexValueIncomplete() throws Throwable { + PromptResult result = prompt(PARAM_INT); + try { + result.h.setArgAsString(PARAM_INT, "0x"); + fail(); + } + catch (NumberFormatException e) { + // pass + } + result.h.invoke(); + } + + @Test + public void testIntSaveHexValue() throws Throwable { + PromptResult result = prompt(PARAM_INT); + result.h.setArgAsString(PARAM_INT, "0x11"); + result.h.invoke(); + + SaveState state = result.h.saveState(); + assertEquals("0x11", state.getString("some_int,java.math.BigInteger", null)); + } + + @Test + @Ignore + public void testIntLoadHexValue() throws Throwable { + /** + * TODO: This is a bit out of order. However, the dialog cannot load/decode from the state + * until it has the parameters. Worse, to check that user input was valid, the dialog + * verifies that the value it gets back matches the text in the box, because if it doesn't, + * then the editor must have failed to parse/decode the value. Currently, loading the state + * while the dialog box has already populated its values, does not modify the contents of + * any editor, so the text will not match, causing this test to fail. + */ + PromptResult result = prompt(PARAM_INT); + SaveState state = new SaveState(); + state.putString("some_int,java.math.BigInteger", "0x11"); + result.h.loadState(state); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_int", intVal(17, "0x11")), args); + } + + @Test + public void testBoolDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_BOOL); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_bool", ValStr.from(true)), args); + } + + @Test + public void testBoolInputValue() throws Throwable { + PromptResult result = prompt(PARAM_BOOL); + result.h.setArg(PARAM_BOOL, false); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_bool", ValStr.from(false)), args); + } + + @Test + public void testPathDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_PATH); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_path", pathVal("my_path")), args); + } + + @Test + public void testPathInputValue() throws Throwable { + PromptResult result = prompt(PARAM_PATH); + result.h.setArgAsString(PARAM_PATH, "your_path"); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_path", pathVal("your_path")), args); + } + + @Test + public void testDirDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_DIR); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_dir", dirVal("my_dir")), args); + } + + @Test + public void testDirInputValue() throws Throwable { + PromptResult result = prompt(PARAM_DIR); + result.h.setArgAsString(PARAM_DIR, "your_dir"); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_dir", dirVal("your_dir")), args); + } + + @Test + public void testFileDefaultValue() throws Throwable { + PromptResult result = prompt(PARAM_FILE); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_file", fileVal("my_file")), args); + } + + @Test + public void testFileInputValue() throws Throwable { + PromptResult result = prompt(PARAM_FILE); + result.h.setArgAsString(PARAM_FILE, "your_file"); + result.h.invoke(); + + Map> args = waitOn(result.args); + assertEquals(Map.of("some_file", fileVal("your_file")), args); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java index 719b30a6f3..dbec222909 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,6 +28,7 @@ import org.junit.Test; import db.Transaction; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.services.TraceRmiLauncherService; +import ghidra.debug.api.ValStr; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; import ghidra.framework.OperatingSystem; @@ -57,11 +58,11 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug protected LaunchConfigurator gdbFileOnly(String file) { return new LaunchConfigurator() { @Override - public Map configureLauncher(TraceRmiLaunchOffer offer, - Map arguments, RelPrompt relPrompt) { - Map args = new HashMap<>(arguments); - args.put("arg:1", new PathIsFile(Paths.get(file))); - args.put("env:OPT_START_CMD", "starti"); + public Map> configureLauncher(TraceRmiLaunchOffer offer, + Map> arguments, RelPrompt relPrompt) { + Map> args = new HashMap<>(arguments); + args.put("arg:1", new ValStr<>(new PathIsFile(Paths.get(file)), file)); + args.put("env:OPT_START_CMD", ValStr.str("starti")); return args; } }; @@ -93,10 +94,10 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug protected LaunchConfigurator dbgengFileOnly(String file) { return new LaunchConfigurator() { @Override - public Map configureLauncher(TraceRmiLaunchOffer offer, - Map arguments, RelPrompt relPrompt) { - Map args = new HashMap<>(arguments); - args.put("env:OPT_TARGET_IMG", new PathIsFile(Paths.get(file))); + public Map> configureLauncher(TraceRmiLaunchOffer offer, + Map> arguments, RelPrompt relPrompt) { + Map> args = new HashMap<>(arguments); + args.put("env:OPT_TARGET_IMG", new ValStr<>(new PathIsFile(Paths.get(file)), file)); return args; } }; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java index ec30950d01..11ffd910dc 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,6 +31,7 @@ import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TestTraceRmiLaunchOpin import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin; import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; +import ghidra.debug.api.ValStr; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult; import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; @@ -182,7 +183,7 @@ public class FlatDebuggerRmiAPITest extends AbstractLiveFlatDebuggerAPITest