diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/PseudoDisassemblerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/PseudoDisassemblerTest.java new file mode 100644 index 0000000000..5425838862 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/PseudoDisassemblerTest.java @@ -0,0 +1,74 @@ +/* ### + * 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.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.*; + +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.listing.Program; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; + +public class PseudoDisassemblerTest extends AbstractGhidraHeadlessIntegrationTest { + + private ProgramBuilder programBuilder;// Instructions are 2-byte aligned + private Program program; + private PseudoDisassembler disassembler; + + private int txId; + + @Before + public void setUp() throws Exception { + programBuilder = new ProgramBuilder("Test", ProgramBuilder._ARM); + program = programBuilder.getProgram(); + txId = program.startTransaction("Add Memory");// leave open until tearDown + programBuilder.createMemory(".text", "0", 64).setExecute(true);// initialized + programBuilder.createUninitializedMemory(".unint", "0x40", 64).setExecute(true);// uninitialized + programBuilder.createUninitializedMemory(".dat", "0x80", 64);// no-execute + programBuilder.createMemory(".text2", "0x3e0", 0x800).setExecute(true);// initialized + + disassembler = new PseudoDisassembler(program); + } + + @After + public void tearDown() throws Exception { + if (program != null) { + program.endTransaction(txId, true); + } + if (programBuilder != null) { + programBuilder.dispose(); + } + } + + @Test + public void testToStringArmSeparator() throws Exception { + programBuilder.setBytes("0", "08 f8 00 00 40 00");// strb.w r0,[r8,r0,0x0] + programBuilder.setRegisterValue("TMode", "0", "1", 1); + PseudoInstruction instr = + disassembler.disassemble(program.getAddressFactory().getAddress("0")); + + String str = instr.toString(); + assertEquals("strb.w r0,[r8,r0,lsl #0x0]", str);// wan't to make sure all markup is printed + + programBuilder.setBytes("0", "00 f0 20 03");// nopeq + programBuilder.setRegisterValue("TMode", "0", "1", 0); + instr = disassembler.disassemble(program.getAddressFactory().getAddress("0")); + + str = instr.toString(); + assertEquals("nopeq", str); + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/imagepanel/ImagePanelTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/imagepanel/ImagePanelTest.java new file mode 100644 index 0000000000..3ad663baa5 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/imagepanel/ImagePanelTest.java @@ -0,0 +1,122 @@ +/* ### + * 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 docking.widgets.imagepanel; + +import static org.junit.Assert.assertTrue; + +import java.awt.Image; + +import javax.swing.Icon; +import javax.swing.JFrame; + +import org.junit.*; + +import docking.test.AbstractDockingTest; +import resources.ResourceManager; +import resources.icons.EmptyIcon; + +public class ImagePanelTest extends AbstractDockingTest { + + private JFrame frame; + private ImagePanel imagePanel; + + @Before + public void setUp() throws Exception { + Icon emptyIcon = new EmptyIcon(32, 32); + Image emptyImage = ResourceManager.getImageIcon(emptyIcon).getImage(); + imagePanel = new ImagePanel(emptyImage); + + frame = new JFrame("ImagePanel Test"); + frame.getContentPane().add(imagePanel); + frame.setSize(400, 400); + + frame.setVisible(true); + } + + @After + public void tearDown() throws Exception { + frame.dispose(); + } + + private void reset() { + imagePanel.setZoomFactor(1.0f); + + assertTrue("Unable to reset zoom factor", + Float.compare(imagePanel.getZoomFactor(), 1.0f) == 0); + + } + + @Test + public void testZoom_Neutral() { + reset(); + + imagePanel.setZoomFactor(1.0f); + + assertTrue("Zoom factor not set to 1.0x", + Float.compare(imagePanel.getZoomFactor(), 1.0f) == 0); + } + + @Test + public void testZoom_10Point0f() { + reset(); + + imagePanel.setZoomFactor(10.0f); + + assertTrue("Zoom factor not set to 10.0x", + Float.compare(imagePanel.getZoomFactor(), 10.0f) == 0); + } + + @Test + public void testZoom_0Point05() { + reset(); + + imagePanel.setZoomFactor(0.05f); + + assertTrue("Zoom factor not set to 0.05x", + Float.compare(imagePanel.getZoomFactor(), 0.05f) == 0); + } + + @Test + public void testZoom_20Point0() { + reset(); + + imagePanel.setZoomFactor(20.0f); + + assertTrue("Zoom factor not set to 20.0x; should be 10.0x", + Float.compare(imagePanel.getZoomFactor(), 10.0f) == 0); + } + + @Test + public void testZoom_0Point001() { + reset(); + + imagePanel.setZoomFactor(0.001f); + + assertTrue("Zoom factor not set to 0.001x; should be 0.05x", + Float.compare(imagePanel.getZoomFactor(), 0.05f) == 0); + } + + @Test + public void testZoom_3Point75() { + reset(); + + imagePanel.setZoomFactor(3.75f); + + assertTrue("Zoom factor not set to 3.75x; should be 4.0x", + Float.compare(imagePanel.getZoomFactor(), 4.0f) == 0); + } + +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/HexOrDecimalInputTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/HexOrDecimalInputTest.java new file mode 100644 index 0000000000..b3a8f904c9 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/HexOrDecimalInputTest.java @@ -0,0 +1,519 @@ +/* ### + * 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 docking.widgets.textfield; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.awt.*; +import java.awt.RenderingHints.Key; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.image.*; +import java.awt.image.renderable.RenderableImage; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +import javax.swing.DebugGraphics; +import javax.swing.RepaintManager; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +public class HexOrDecimalInputTest { + + @Test + public void testSetAllowNegative() { + HexOrDecimalInput input = new HexOrDecimalInput(); + + Long newValue = -1L; + input.setValue(newValue); + + assertThat(input.getValue(), is(newValue)); + + input.setAllowNegative(false); + assertThat(input.getValue(), nullValue()); + + newValue = 20L; + input.setValue(newValue); + assertThat(input.getValue(), is(newValue)); + + newValue = -100L; + input.setValue(newValue); + assertThat(input.getValue(), nullValue()); + + input.setAllowNegative(true); + newValue = -100L; + input.setValue(newValue); + assertThat(input.getValue(), is(newValue)); + } + + @Test + public void testCustomPaint() { + + HexOrDecimalInput input = new HexOrDecimalInput(); + RepaintManager repaintManager = RepaintManager.currentManager(input); + repaintManager.setDoubleBufferingEnabled(false); + + SpyPrintStream spy = new SpyPrintStream(); + DebugGraphics.setLogStream(spy); + + DebugGraphics debugGraphics = new DebugGraphics(scratchGraphics()); + debugGraphics.setDebugOptions(DebugGraphics.LOG_OPTION); + + Graphics2D g2d = new Graphics2DAdapter(debugGraphics); + input.paintComponent(g2d); + assertThat(spy.toString(), CoreMatchers.containsString("Dec")); + + spy.reset(); + input.setHexMode(); + input.paintComponent(g2d); + assertThat(spy.toString(), CoreMatchers.containsString("Hex")); + + spy.reset(); + input.setDecimalMode(); + input.paintComponent(g2d); + assertThat(spy.toString(), CoreMatchers.containsString("Dec")); + } + + @Test + public void testToggleHexModeFromKeybinding() { + HexOrDecimalInput input = new HexOrDecimalInput(); + Long value = 10L; + input.setValue(value); + + assertThat(input.getValue(), is(value)); + + toggleMode(input); + + assertThat(input.getValue(), is(0xAL)); + + toggleMode(input); + + assertThat(input.getValue(), is(value)); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private void toggleMode(final HexOrDecimalInput input) { + KeyEvent event = new KeyEvent(input, 0, System.currentTimeMillis(), 0, KeyEvent.VK_M, 'm'); + KeyListener[] keyListeners = input.getKeyListeners(); + for (KeyListener listener : keyListeners) { + listener.keyPressed(event); + } + } + + private Graphics scratchGraphics() { + BufferedImage image = new BufferedImage(100, 20, BufferedImage.TYPE_INT_BGR); + return image.getGraphics(); + } + + private static class SpyPrintStream extends PrintStream { + private static ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + SpyPrintStream() { + super(baos); + } + + void reset() { + baos.reset(); + } + + @Override + public String toString() { + return baos.toString(); + } + } + + /** + * An adapter to turn a Graphics into a Graphics2D. This implementation satisfies the base + * methods needed for the test. So, many operations are stubbed or are exceptional. + */ + private static class Graphics2DAdapter extends Graphics2D { + + private Graphics delegate; + + Graphics2DAdapter(Graphics g) { + this.delegate = g; + } + + @Override + public void draw(Shape s) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawRenderableImage(RenderableImage img, AffineTransform xform) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawString(String str, int x, int y) { + delegate.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + delegate.drawString(str, (int) x, (int) y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, int x, int y) { + delegate.drawString(iterator, x, y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + delegate.drawString(iterator, (int) x, (int) y); + } + + @Override + public void drawGlyphVector(GlyphVector g, float x, float y) { + throw new UnsupportedOperationException(); + } + + @Override + public void fill(Shape s) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hit(Rectangle rect, Shape s, boolean onStroke) { + throw new UnsupportedOperationException(); + } + + @Override + public GraphicsConfiguration getDeviceConfiguration() { + throw new UnsupportedOperationException(); + } + + @Override + public void setComposite(Composite comp) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPaint(Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void setStroke(Stroke s) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRenderingHint(Key hintKey, Object hintValue) { + // no-op + } + + @Override + public Object getRenderingHint(Key hintKey) { + return null; + } + + @Override + public void setRenderingHints(Map hints) { + // no-op + } + + @Override + public void addRenderingHints(Map hints) { + throw new UnsupportedOperationException(); + } + + @Override + public RenderingHints getRenderingHints() { + throw new UnsupportedOperationException(); + } + + @Override + public void translate(int x, int y) { + throw new UnsupportedOperationException(); + } + + @Override + public void translate(double tx, double ty) { + throw new UnsupportedOperationException(); + } + + @Override + public void rotate(double theta) { + throw new UnsupportedOperationException(); + } + + @Override + public void rotate(double theta, double x, double y) { + throw new UnsupportedOperationException(); + } + + @Override + public void scale(double sx, double sy) { + throw new UnsupportedOperationException(); + } + + @Override + public void shear(double shx, double shy) { + throw new UnsupportedOperationException(); + } + + @Override + public void transform(AffineTransform Tx) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTransform(AffineTransform Tx) { + throw new UnsupportedOperationException(); + } + + @Override + public AffineTransform getTransform() { + throw new UnsupportedOperationException(); + } + + @Override + public Paint getPaint() { + throw new UnsupportedOperationException(); + } + + @Override + public Composite getComposite() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBackground(Color color) { + throw new UnsupportedOperationException(); + } + + @Override + public Color getBackground() { + throw new UnsupportedOperationException(); + } + + @Override + public Stroke getStroke() { + throw new UnsupportedOperationException(); + } + + @Override + public void clip(Shape s) { + throw new UnsupportedOperationException(); + } + + @Override + public FontRenderContext getFontRenderContext() { + throw new UnsupportedOperationException(); + } + + @Override + public Graphics create() { + return delegate.create(); + } + + @Override + public Color getColor() { + return delegate.getColor(); + } + + @Override + public void setColor(Color c) { + delegate.setColor(c); + } + + @Override + public void setPaintMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setXORMode(Color c1) { + delegate.setXORMode(c1); + } + + @Override + public Font getFont() { + return delegate.getFont(); + } + + @Override + public void setFont(Font font) { + delegate.setFont(font); + } + + @Override + public FontMetrics getFontMetrics(Font f) { + return delegate.getFontMetrics(); + } + + @Override + public Rectangle getClipBounds() { + return delegate.getClipBounds(); + } + + @Override + public void clipRect(int x, int y, int width, int height) { + delegate.clipRect(x, y, width, height); + } + + @Override + public void setClip(int x, int y, int width, int height) { + delegate.setClip(x, y, width, height); + } + + @Override + public Shape getClip() { + return delegate.getClip(); + } + + @Override + public void setClip(Shape clip) { + delegate.setClip(clip); + } + + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + delegate.copyArea(x, y, width, height, dx, dy); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + delegate.drawLine(x1, y1, x2, y2); + } + + @Override + public void fillRect(int x, int y, int width, int height) { + delegate.fillRect(x, y, width, height); + } + + @Override + public void clearRect(int x, int y, int width, int height) { + delegate.clearRect(x, y, width, height); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, + int arcHeight) { + delegate.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, + int arcHeight) { + delegate.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + delegate.drawOval(x, y, width, height); + } + + @Override + public void fillOval(int x, int y, int width, int height) { + delegate.fillOval(x, y, width, height); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + delegate.drawArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + delegate.fillArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + delegate.drawPolyline(xPoints, yPoints, nPoints); + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + delegate.drawPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + delegate.fillPolygon(null); + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return delegate.drawImage(img, x, y, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, + ImageObserver observer) { + return delegate.drawImage(img, x, y, width, height, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return delegate.drawImage(img, x, y, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, + ImageObserver observer) { + return delegate.drawImage(img, x, y, width, height, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, + int sx2, int sy2, ImageObserver observer) { + return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, + int sx2, int sy2, Color bgcolor, ImageObserver observer) { + return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, + observer); + } + + @Override + public void dispose() { + delegate.dispose(); + } + + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java new file mode 100644 index 0000000000..d708536be5 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/textfield/IntegerTextFieldTest.java @@ -0,0 +1,290 @@ +/* ### + * 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 docking.widgets.textfield; + +import static org.junit.Assert.*; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.junit.*; + +import docking.test.AbstractDockingTest; + +public class IntegerTextFieldTest extends AbstractDockingTest { + + private JFrame frame; + private IntegerTextField field; + private JTextField textField; + + @Before + public void setUp() throws Exception { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + field = new IntegerTextField(10); + field.setShowNumberMode(true); + textField = (JTextField) field.getComponent(); + frame = new JFrame("Test"); + frame.getContentPane().add(field.getComponent()); + frame.pack(); + frame.setVisible(true); + } + + @After + public void tearDown() throws Exception { + frame.setVisible(false); + } + + @Test + public void testDefaultState() { + assertNull(field.getValue());// no value + assertEquals(0, field.getIntValue());// the "int value" return for null is 0 + assertEquals(0, field.getLongValue()); + assertTrue(!field.isHexMode()); + assertNull(field.getMaxValue()); + } + + @Test + public void testTypeValidDecimalNumber() { + triggerText(textField, "123"); + assertEquals(123, field.getIntValue()); + } + + @Test + public void testTypeValidHexNumber() { + triggerText(textField, "0x2abcdef"); + assertEquals(0x2abcdef, field.getIntValue()); + } + + @Test + public void testInvalidCharsIgnored() { + triggerText(textField, "123ghijklmnopqrstuvwxyz4"); + assertEquals(1234, field.getIntValue()); + } + + @Test + public void testHexCharsIgnoredInDecimalMode() { + assertTrue(!field.isHexMode()); + triggerText(textField, "123ghijklmnopqrstuvwxyz4"); + assertEquals(1234, field.getIntValue()); + } + + @Test + public void testXchangesHexMode() { + assertTrue(!field.isHexMode()); + triggerText(textField, "0"); + assertTrue(!field.isHexMode()); + triggerText(textField, "x"); + assertTrue(field.isHexMode()); + triggerBackspaceKey(textField); + assertTrue(!field.isHexMode()); + } + + @Test + public void testHexModeWithoutPrefix() { + triggerText(textField, "abc");// not allowed when using hex prefix, so expect empty + assertEquals(null, field.getValue()); + + field.setAllowsHexPrefix(false); + field.setHexMode(); + triggerText(textField, "abc"); + assertEquals(0xabc, field.getIntValue()); + } + + @Test + public void testNegative() { + triggerText(textField, "-123"); + assertEquals(-123, field.getIntValue()); + } + + @Test + public void testNegativeHex() { + triggerText(textField, "-0xa"); + assertEquals(-10, field.getIntValue()); + } + + @Test + public void testNegativeNotAllowed() { + field.setAllowNegativeValues(false); + triggerText(textField, "-123"); + assertEquals(123, field.getIntValue()); + } + + @Test + public void testSetNegativeWithCurrentNegativeValue() { + field.setValue(-123); + field.setAllowNegativeValues(false); + assertEquals(null, field.getValue()); + } + + @Test + public void testMax() { + field.setMaxValue(BigInteger.valueOf(13l)); + triggerText(textField, "12"); + assertEquals(12, field.getIntValue()); + + field.setValue(null); + triggerText(textField, "13"); + assertEquals(13, field.getIntValue()); + + field.setValue(null); + triggerText(textField, "14");// four should be ignored + assertEquals(1, field.getIntValue()); + + } + + @Test + public void testSetMaxToValueSmallerThanCurrent() { + field.setValue(500); + field.setMaxValue(BigInteger.valueOf(400)); + assertEquals(400, field.getIntValue()); + } + + @Test + public void testMaxInHex() { + field.setMaxValue(BigInteger.valueOf(0xd)); + triggerText(textField, "0xc"); + assertEquals(12, field.getIntValue()); + + field.setValue(null); + triggerText(textField, "0xd"); + assertEquals(13, field.getIntValue()); + + field.setValue(null); + triggerText(textField, "0xe");// e should be ignored + assertEquals(0, field.getIntValue()); + + } + + @Test + public void testSwitchingHexMode() { + field.setValue(255); + assertEquals("255", field.getText()); + field.setHexMode(); + assertEquals("0xff", field.getText()); + field.setDecimalMode(); + assertEquals("255", field.getText()); + } + + @Test + public void testChangeListenerAfterValidInput() { + TestChangeListener listener = new TestChangeListener(); + field.addChangeListener(listener); + + triggerText(textField, "123"); + assertEquals(3, listener.count); + assertEquals(1, listener.values.get(0)); + assertEquals(12, listener.values.get(1)); + assertEquals(123, listener.values.get(2)); + + triggerBackspaceKey(textField); + assertEquals(12, listener.values.get(3)); + + } + + @Test + public void testChangeListenerAfterSwitchingModes() { + triggerText(textField, "123"); + + TestChangeListener listener = new TestChangeListener(); + field.addChangeListener(listener); + + setHexMode(); + + assertEquals(2, listener.count); + assertEquals(123, listener.values.get(1)); + + } + + @Test + public void testNegativeHexFromValue() { + field.setValue(-255); + setHexMode(); + assertEquals("-0xff", field.getText()); + } + + @Test + public void testNullValue() { + field.setValue(12); + assertEquals("12", field.getText()); + field.setValue(null); + assertEquals("", field.getText()); + assertEquals(0, field.getIntValue()); + assertEquals(0l, field.getLongValue()); + assertEquals(null, field.getValue()); + } + + @Test + public void testHexValueInDontRequireHexPrefixMode() { + field.setAllowsHexPrefix(false); + field.setHexMode(); + field.setValue(255); + assertEquals("ff", field.getText()); + } + + @Test + public void testSetNotAllowNegativeModeWhileCurrentValueIsNegative() { + field.setValue(-10); + field.setAllowNegativeValues(false); + assertEquals("", field.getText()); + assertEquals(0, field.getIntValue()); + } + + @Test + public void testSetLongValue() { + field.setValue(100L); + assertEquals(100L, field.getLongValue()); + assertEquals(100, field.getIntValue()); + } + + @Test + public void testSettingNegativeNumberWhenNegativesArentAllowed() { + field.setValue(10); + field.setAllowNegativeValues(false); + field.setValue(-10); + assertEquals("", field.getText()); + } + + @Test + public void testUseHexPrefixUpdatesTextField() { + field.setAllowsHexPrefix(false); + field.setHexMode(); + field.setValue(255); + assertEquals("ff", field.getText()); + field.setAllowsHexPrefix(true); + assertEquals("0xff", field.getText()); + } + + private void setHexMode() { + runSwing(() -> field.setHexMode()); + waitForSwing(); + } + + class TestChangeListener implements ChangeListener { + volatile int count; + private AtomicIntegerArray values = new AtomicIntegerArray(10); + + @Override + public void stateChanged(ChangeEvent e) { + values.set(count++, field.getIntValue()); + } + + } + +} diff --git a/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskMonitorSplitterTest.java b/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskMonitorSplitterTest.java new file mode 100644 index 0000000000..58f266bbf4 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskMonitorSplitterTest.java @@ -0,0 +1,67 @@ +/* ### + * 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.util.task; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import docking.test.AbstractDockingTest; +import generic.test.AbstractGenericTest; + +public class TaskMonitorSplitterTest extends AbstractDockingTest { + TaskMonitor baseMonitor; + + public TaskMonitorSplitterTest() { + super(); + baseMonitor = new TaskMonitorComponent(); + } + + @Test + public void testBasicUse() { + TaskMonitor[] monitors = TaskMonitorSplitter.splitTaskMonitor(baseMonitor, 4); + + monitors[0].initialize(100); + monitors[0].setProgress(1); + assertEquals(1, monitors[0].getProgress()); + assertEquals(TaskMonitorSplitter.MONITOR_SIZE / 400, baseMonitor.getProgress()); + + monitors[0].incrementProgress(1); + assertEquals(2 * TaskMonitorSplitter.MONITOR_SIZE / 400, baseMonitor.getProgress()); + + monitors[0].setProgress(10); + assertEquals(10 * TaskMonitorSplitter.MONITOR_SIZE / 400, baseMonitor.getProgress()); + + } + + @Test + public void testMaxSettings() { + TaskMonitor[] monitors = TaskMonitorSplitter.splitTaskMonitor(baseMonitor, 4); + + monitors[0].initialize(100); + monitors[0].setProgress(50); + assertEquals(50 * TaskMonitorSplitter.MONITOR_SIZE / 400, baseMonitor.getProgress()); + + monitors[0].setMaximum(25); + assertEquals(25, monitors[0].getMaximum()); + assertEquals(TaskMonitorSplitter.MONITOR_SIZE / 4, baseMonitor.getProgress()); + + monitors[0].setMaximum(100); + assertEquals(25 * TaskMonitorSplitter.MONITOR_SIZE / 400, baseMonitor.getProgress()); + + } + +} diff --git a/Ghidra/Framework/Help/src/test/java/help/AbstractHelpTest.java b/Ghidra/Framework/Help/src/test/java/help/AbstractHelpTest.java new file mode 100644 index 0000000000..8d8a3cdd73 --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/AbstractHelpTest.java @@ -0,0 +1,196 @@ +/* ### + * 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 help; + +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.nio.file.*; + +import org.junit.After; +import org.junit.Before; + +import generic.test.AbstractGenericTest; +import utilities.util.FileUtilities; + +public abstract class AbstractHelpTest extends AbstractGenericTest { + + protected static final String HELP_FILENAME_PREFIX = "Fake"; + protected static final String HELP_FILENAME = HELP_FILENAME_PREFIX + ".html"; + + private Path testTempDir; + + public AbstractHelpTest() { + super(); + } + + @Before + public void setUp() throws Exception { + + testTempDir = Files.createTempDirectory(testName.getMethodName()); + } + + @After + public void tearDown() throws Exception { + FileUtilities.deleteDir(testTempDir.toFile()); + } + + protected Path createHelpBuildOutputDir() throws IOException { + Path out = testTempDir.resolve("build/help/main/help"); + Files.createDirectories(out); + return out; + } + + protected Path createFakeHelpTopic(Path helpDir) throws IOException { + return createFakeHelpTopic("FakeTopic", helpDir); + } + + protected Path createFakeHelpTopic(String topicName, Path helpDir) throws IOException { + Path topicsDir = helpDir.resolve("topics"); + Path fakeTopicDir = topicsDir.resolve(topicName); + Files.createDirectories(fakeTopicDir); + return fakeTopicDir; + } + + protected Path createTempHelpDir() throws IOException { + Path helpDir = testTempDir.resolve("help"); + Files.createDirectory(helpDir); + return helpDir; + } + + protected void addRequiredHelpDirStructure(Path helpDir) throws IOException { + + // HelpFile wants to read one of these, so put one there + createEmpty_TOC_Source_File(helpDir); + createSharedDir(helpDir); + } + + protected Path createSharedDir(Path helpDir) throws IOException { + Path sharedDir = helpDir.resolve("shared"); + Files.createDirectory(sharedDir); + + Path css = sharedDir.resolve("Frontpage.css"); + Files.createFile(css); + + Path png = sharedDir.resolve("test.png"); + Files.createFile(png); + + return sharedDir; + } + + protected Path createEmpty_TOC_Source_File(Path dir) throws IOException { + + Path fullTOCPath = dir.resolve("TOC_Source.xml"); + Path file = Files.createFile(fullTOCPath); + + //@formatter:off + String TOCXML = "\n" + + "\n\n" + + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, TOCXML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + protected Path createHelpContent(Path topic, String anchor) throws IOException { + Path htmlPath = topic.resolve(HELP_FILENAME); + Path file = Files.createFile(htmlPath); + + if (anchor == null) { + anchor = "Default_Anchor"; + } + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + + "\n" + + "

Configure Tool

\n" + + " Some text with reference to shared image \n" + + " \n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + protected Path createHelpContent_WithReferenceHREF(Path topic, String HREF) throws IOException { + Path htmlPath = topic.resolve(HELP_FILENAME); + Path file = Files.createFile(htmlPath); + + assertNotNull("Must specify the A tag HREF attribute", HREF); + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + + "\n" + + "

Configure Tool

\n" + + " And this is a link Click Me" + + " \n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + protected Path createHelpContent_WithReferenceIMG_SRC(Path topic, String SRC) + throws IOException { + Path htmlPath = topic.resolve(HELP_FILENAME); + Path file = Files.createFile(htmlPath); + + assertNotNull("Must specify the A tag SRC attribute", SRC); + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + + "\n" + + "

Configure Tool

\n" + + " Some text with reference to shared image \n" + + " \n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + protected void copy(Path from, Path to) throws Exception { + + FileUtilities.copyDir(from.toFile(), to.toFile(), null); + } +} diff --git a/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java b/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java new file mode 100644 index 0000000000..acc7faf851 --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java @@ -0,0 +1,99 @@ +/* ### + * 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 help; + +import static org.junit.Assert.*; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.Test; + +public class HelpBuildUtilsTest extends AbstractHelpTest { + + private static final String HELP_TOPIC_PATH = "/some/fake/path/to/help/topics"; + private static final String TOPIC_AND_FILENAME = "FooTopic/FooFile.html"; + private static final String HTML_FILE_PATH = HELP_TOPIC_PATH + '/' + TOPIC_AND_FILENAME; + + public HelpBuildUtilsTest() { + super(); + } + + @Test + public void testGetRelativeHelpPath() { + String relativeString = "help/topics/FooTopic/FooFile.html"; + Path path = Paths.get("/some/fake/path/to/" + relativeString); + Path relative = HelpBuildUtils.relativizeWithHelpTopics(path); + assertEquals(relativeString, relative.toString()); + } + + @Test + public void testGetRelativeHelpPath_NoHelpTopicInPath() { + String invalidRelativeString = "help/topicz/" + TOPIC_AND_FILENAME; + Path path = Paths.get("/some/fake/path/to/" + invalidRelativeString); + Path relative = HelpBuildUtils.relativizeWithHelpTopics(path); + assertNull(relative); + } + + @Test + public void testLocateReference_Local_HelpSystemSyntax() throws URISyntaxException { + Path sourceFile = Paths.get(HTML_FILE_PATH); + String reference = "help/topics/shared/foo.png"; + Path resolved = HelpBuildUtils.locateReference(sourceFile, reference); + assertEquals("Help System syntax was not preserved", Paths.get(reference), resolved); + } + + @Test + public void testLocateReference_Local_RelativeSyntax() throws URISyntaxException { + Path sourceFile = Paths.get(HTML_FILE_PATH); + String reference = "../shared/foo.png";// go up one to the help dir + Path resolved = HelpBuildUtils.locateReference(sourceFile, reference); + assertEquals("Relative syntax did not locate file", + Paths.get(HELP_TOPIC_PATH + "/shared/foo.png"), resolved); + } + + @Test + public void testLocateReference_Remote() throws URISyntaxException { + Path sourceFile = Paths.get(HTML_FILE_PATH); + String reference = "http://some.fake.server/foo.png"; + Path resolved = HelpBuildUtils.locateReference(sourceFile, reference); + assertNull(resolved); + boolean isRemote = HelpBuildUtils.isRemote(reference); + assertTrue(isRemote); + } + + @Test + public void testLocateReferences_Icons() throws URISyntaxException { + Path sourceFile = Paths.get(HTML_FILE_PATH); + String reference = "Icons.REFRESH_ICON"; // see Icons class + ImageLocation location = HelpBuildUtils.locateImageReference(sourceFile, reference); + Path resolved = location.getResolvedPath(); + String name = resolved.getFileName().toString(); + assertEquals("Help System syntax was not preserved", "reload3.png", name); + assertTrue(location.isRuntime()); + assertFalse(location.isRemote()); + } + + @Test + public void testLocateReferences_Icons_BadName() throws URISyntaxException { + Path sourceFile = Paths.get(HTML_FILE_PATH); + String reference = "Icons.REFRESH_ICON_BAD"; // non-existent + ImageLocation location = HelpBuildUtils.locateImageReference(sourceFile, reference); + Path resolved = location.getResolvedPath(); + assertNull(resolved); + } +} diff --git a/Ghidra/Framework/Help/src/test/java/help/OverlayHelpTreeTest.java b/Ghidra/Framework/Help/src/test/java/help/OverlayHelpTreeTest.java new file mode 100644 index 0000000000..20b41f9c57 --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/OverlayHelpTreeTest.java @@ -0,0 +1,418 @@ +/* ### + * 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 help; + +import static org.junit.Assert.assertEquals; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.help.HelpSet; + +import org.junit.Test; + +import help.validator.LinkDatabase; +import help.validator.location.*; +import help.validator.model.*; + +public class OverlayHelpTreeTest { + + @Test + public void testSourceTOCFileThatDependsUponPreBuiltHelp() { + // + // We want to make sure the overlay tree will properly resolve help TOC items being + // built from TOC_Source.xml files when that file uses items that are defined + // in a help that lives inside of a pre-built jar file. + // + /* + + Example makeup we will create: + + PreBuild_TOC.xml + + + + + + + TOC_Source.xml + + + + + + + + */ + + TOCItemExternal root = externalItem("root"); + TOCItemExternal child_1 = externalItem(root, "child_1"); + + Path tocSourceFile = Paths.get("/fake/path_2/TOC_Source.xml"); + String root_ID = root.getIDAttribute(); + TOCItemReference root_ref = referenceItem(root_ID, tocSourceFile); + + String child_1_ID = child_1.getIDAttribute(); + TOCItemReference child_1_ref = referenceItem(root_ref, child_1_ID, tocSourceFile); + + TOCItemDefinition child_2 = definitionItem(child_1_ref, "child_2", tocSourceFile); + + TOCItemProviderTestStub tocProvider = new TOCItemProviderTestStub(); + tocProvider.addExternal(root); + tocProvider.addExternal(child_1); + tocProvider.addDefinition(child_2); + + TOCSpyWriter spy = printOverlayTree(tocProvider, tocSourceFile); + + assertNodeCount(spy, 3); + assertOrder(spy, 1, root); + assertOrder(spy, 2, child_1); + assertOrder(spy, 3, child_2); + } + + @Test + public void testSourceTOCFileThatDependsUponPreBuiltHelp_MultiplePreBuiltInputs() { + // + // We want to make sure the overlay tree will properly resolve help TOC items being + // built from TOC_Source.xml files when that file uses items that are defined + // in a help that lives inside of multiple pre-built jar files. + // + /* + + Example makeup we will create: + + PreBuild_TOC.xml + + + + + + + + Another PreBuild_TOC.xml + + + + + + + + + TOC_Source.xml + + + + + + + + */ + + TOCItemExternal root_a = externalItem("root"); + TOCItemExternal child_1_a = externalItem(root_a, "child_1"); + TOCItemExternal prebuilt_a_child = externalItem(child_1_a, "prebuilt_a_child"); + + // note: same ID values, since they represent the same nodes, but from different TOC files + TOCItemExternal root_b = externalItemAlt(null, "root"); + TOCItemExternal child_1_b = externalItemAlt(root_b, "child_1"); + TOCItemExternal prebuilt_b_child = externalItemAlt(child_1_b, "prebuilt_b_child"); + + Path tocSourceFile = Paths.get("/fake/path_2/TOC_Source.xml"); + String root_ID = root_a.getIDAttribute(); + TOCItemReference root_ref = referenceItem(root_ID, tocSourceFile); + + String child_1_ID = child_1_a.getIDAttribute(); + TOCItemReference child_1_ref = referenceItem(root_ref, child_1_ID, tocSourceFile); + + TOCItemDefinition child_2 = definitionItem(child_1_ref, "child_2", tocSourceFile); + + TOCItemProviderTestStub tocProvider = new TOCItemProviderTestStub(); + tocProvider.addExternal(root_a); + tocProvider.addExternal(root_b); + tocProvider.addExternal(child_1_a); + tocProvider.addExternal(child_1_b); + tocProvider.addExternal(prebuilt_a_child); + tocProvider.addExternal(prebuilt_b_child); + tocProvider.addDefinition(child_2); + + TOCSpyWriter spy = printOverlayTree(tocProvider, tocSourceFile); + + assertNodeCount(spy, 3); + assertOrder(spy, 1, root_a);// could also be root_b, same ID + assertOrder(spy, 2, child_1_a);// could also be child_1_b, same ID + assertOrder(spy, 3, child_2); + + // note: prebuilt_a_child and prebuilt_b_child don't get output, since they do not have + // the same TOC file ID as the help file being processed (in other words, they don't + // live in the TOC_Source.xml being processes, so they are not part of the output). + } + + @Test + public void testSourceTOCFileThatDependsAnotherTOCSourceFile() { + + /* + + The first source file defines attributes that the second file references. + + Example makeup we will create: + + TOC_Source.xml + + + + + + + Another TOC_Source.xml + + + + + + + + */ + + Path toc_1 = Paths.get("/fake/path_1/TOC_Source.xml"); + TOCItemDefinition root = definitionItem("root", toc_1); + TOCItemDefinition child_1 = definitionItem(root, "child_1", toc_1); + + Path toc_2 = Paths.get("/fake/path_2/TOC_Source.xml"); + String root_ID = root.getIDAttribute(); + String child_1_ID = child_1.getIDAttribute(); + + TOCItemReference root_ref = referenceItem(root_ID, toc_2); + TOCItemReference child_1_ref = referenceItem(root_ref, child_1_ID, toc_2); + TOCItemDefinition child_2 = definitionItem(child_1_ref, "child_2", toc_2); + + TOCItemProviderTestStub tocProvider = new TOCItemProviderTestStub(); + tocProvider.addDefinition(root); + tocProvider.addDefinition(child_1); + tocProvider.addDefinition(child_2);// in the second TOC file + + TOCSpyWriter spy = printOverlayTree(tocProvider, toc_2); + + assertNodeCount(spy, 3); + assertOrder(spy, 1, root); + assertOrder(spy, 2, child_1); + assertOrder(spy, 3, child_2); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private TOCSpyWriter printOverlayTree(TOCItemProviderTestStub tocItemProvider, Path tocFile) { + + // + // Create a test version of the LinkDatabase for the overlay tree, with test versions of + // it's required TOC input file and HelpModuleLocation + // + GhidraTOCFileDummy toc = new GhidraTOCFileDummy(tocFile); + OverlayHelpModuleLocationTestStub location = new OverlayHelpModuleLocationTestStub(toc); + LinkDatabaseTestStub db = new LinkDatabaseTestStub(location); + + // This is the class we are testing!! + OverlayHelpTree overlayHelpTree = new OverlayHelpTree(tocItemProvider, db); + + TOCSpyWriter spy = new TOCSpyWriter(); + String TOCID = tocFile.toUri().toString(); + overlayHelpTree.printTreeForID(spy, TOCID); + + System.out.println(spy.toString()); + + return spy; + } + + private TOCItemDefinition definitionItem(String ID, Path tocSourceFile) { + return definitionItem(null, ID, tocSourceFile); + } + + private TOCItemDefinition definitionItem(TOCItem parent, String ID, Path tocSourceFile) { + String target = "fake"; + String sort = ""; + int line = 1; + return new TOCItemDefinition(parent, tocSourceFile, ID, ID, target, sort, line); + } + + private TOCItemReference referenceItem(String referenceID, Path tocSourceFile) { + return referenceItem(null, referenceID, tocSourceFile); + } + + private TOCItemReference referenceItem(TOCItem parent, String referenceID, Path tocSourceFile) { + return new TOCItemReference(parent, tocSourceFile, referenceID, 1); + } + + private TOCItemExternal externalItem(String ID) { + return externalItem(null, ID); + } + + private TOCItemExternal externalItem(TOCItem parent, String ID) { + Path tocFile = Paths.get("/fake/path_1/PreBuild_TOC.xml"); + String target = "fake"; + String sort = ""; + int line = 1; + return new TOCItemExternal(parent, tocFile, ID, ID, target, sort, line); + } + + private TOCItemExternal externalItemAlt(TOCItem parent, String ID) { + Path tocFile = Paths.get("/fake/path_1/PreBuild_TOC.xml"); + String target = "fake"; + String sort = ""; + int line = 1; + return new TOCItemExternal(parent, tocFile, ID, ID, target, sort, line); + } + + private void assertOrder(TOCSpyWriter spy, int ordinal, TOCItem item) { + String ID = spy.getItem(ordinal - 1 /* make an index */); + assertEquals("Did not find TOC item at expected index: " + ordinal, item.getIDAttribute(), + ID); + } + + private void assertNodeCount(TOCSpyWriter spy, int count) { + assertEquals("Did not get exactly one node per TOC item input", count, spy.getItemCount()); + } + + private class TOCSpyWriter extends PrintWriter { + + private StringWriter stringWriter; + + private List tocItems = new ArrayList<>(); + + public TOCSpyWriter() { + super(new StringWriter(), true); + stringWriter = ((StringWriter) out); + } + + String getItem(int position) { + return tocItems.get(position); + } + + int getItemCount() { + return tocItems.size(); + } + + @Override + public void println(String s) { + super.println(s); + + s = s.trim(); + if (!s.startsWith(" externals = new HashMap<>(); + Map definitions = new HashMap<>(); + + void addExternal(TOCItemExternal item) { + String displayText = item.getIDAttribute(); + externals.put(displayText, item); + } + + void addDefinition(TOCItemDefinition item) { + String ID = item.getIDAttribute(); + definitions.put(ID, item); + } + + @Override + public Map getTOCItemExternalsByDisplayMapping() { + return externals; + } + + @Override + public Map getTOCItemDefinitionsByIDMapping() { + return definitions; + } + + } + + private class LinkDatabaseTestStub extends LinkDatabase { + + public LinkDatabaseTestStub(HelpModuleLocation loc) { + super(HelpModuleCollection.fromHelpLocations(Collections.singleton(loc))); + } + + @Override + public String getIDForLink(String target) { + return "test_ID_" + target; + } + } + + private class OverlayHelpModuleLocationTestStub extends HelpModuleLocationTestDouble { + + OverlayHelpModuleLocationTestStub(GhidraTOCFileDummy toc) { + super(Paths.get("/fake/help")); + this.sourceTOCFile = toc; + } + + @Override + protected void loadHelpTopics() { + // no! ...don't really go to the filesystem + } + + @Override + public GhidraTOCFile loadSourceTOCFile() { + return null;// we set this in the constructor + } + + @Override + public HelpSet loadHelpSet() { + return null; + } + + @Override + public boolean isHelpInputSource() { + return true; + } + + } + + private class GhidraTOCFileDummy extends GhidraTOCFileTestDouble { + + public GhidraTOCFileDummy(Path path) { + super(path); + } + } +} diff --git a/Ghidra/Framework/Help/src/test/java/help/validator/location/HelpModuleLocationTestDouble.java b/Ghidra/Framework/Help/src/test/java/help/validator/location/HelpModuleLocationTestDouble.java new file mode 100644 index 0000000000..40ba4d5cdf --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/validator/location/HelpModuleLocationTestDouble.java @@ -0,0 +1,28 @@ +/* ### + * 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 help.validator.location; + +import java.nio.file.Path; + +import help.validator.location.HelpModuleLocation; + +public abstract class HelpModuleLocationTestDouble extends HelpModuleLocation { + + // this class exists to open up the package-level constructor + public HelpModuleLocationTestDouble(Path source) { + super(source); + } +} diff --git a/Ghidra/Framework/Help/src/test/java/help/validator/model/AnchorDefinitionTest.java b/Ghidra/Framework/Help/src/test/java/help/validator/model/AnchorDefinitionTest.java new file mode 100644 index 0000000000..000b1e1a37 --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/validator/model/AnchorDefinitionTest.java @@ -0,0 +1,70 @@ +/* ### + * 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 help.validator.model; + +import static org.junit.Assert.assertEquals; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.Assert; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.util.exception.AssertException; + +public class AnchorDefinitionTest extends AbstractGenericTest { + + public AnchorDefinitionTest() { + super(); + } + +@Test + public void testFileWithoutAnchor() { + // this should generate and ID that is the filename only + Path fullPath = Paths.get("/fake/full/path/help/topics/TopicName/HelpFilename.html"); // dir case + AnchorDefinition def = new AnchorDefinition(fullPath, null, 1); + assertEquals("TopicName_HelpFilename", def.getId()); + } + +@Test + public void testFileInHelpTopicDir() { + + Path fullPath = Paths.get("/fake/full/path/help/topics/TopicName/HelpFilename.html"); // dir case + AnchorDefinition def = new AnchorDefinition(fullPath, "anchor_1", 1); + assertEquals("TopicName_anchor_1", def.getId()); + } + +@Test + public void testFileInHelpTopicJar() { + Path fullPath = Paths.get("/help/topics/TopicName/HelpFilename.html"); // jar case + AnchorDefinition def = new AnchorDefinition(fullPath, "anchor_1", 1); + assertEquals("TopicName_anchor_1", def.getId()); + } + +@Test + public void testFileInHelpDir_NotUnderHelpTopic() { + Path fullPath = Paths.get("/fake/full/path/help/HelpFilename.html"); // dir case + + try { + new AnchorDefinition(fullPath, "anchor_1", 1); + Assert.fail("Did not fail with file not living under a help topic directory"); + } + catch (AssertException e) { + // good + } + } +} diff --git a/Ghidra/Framework/Help/src/test/java/help/validator/model/GhidraTOCFileTestDouble.java b/Ghidra/Framework/Help/src/test/java/help/validator/model/GhidraTOCFileTestDouble.java new file mode 100644 index 0000000000..d54bda407a --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/validator/model/GhidraTOCFileTestDouble.java @@ -0,0 +1,28 @@ +/* ### + * IP: GHIDRA + * REVIEWED: YES + * + * 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 help.validator.model; + +import java.nio.file.Path; + +public class GhidraTOCFileTestDouble extends GhidraTOCFile { + + // this class exists to open up constructor access + public GhidraTOCFileTestDouble(Path sourceFile) { + super(sourceFile); + } + +} diff --git a/Ghidra/Framework/Help/src/test/java/help/validator/model/HelpFileTest.java b/Ghidra/Framework/Help/src/test/java/help/validator/model/HelpFileTest.java new file mode 100644 index 0000000000..db76305442 --- /dev/null +++ b/Ghidra/Framework/Help/src/test/java/help/validator/model/HelpFileTest.java @@ -0,0 +1,307 @@ +/* ### + * 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 help.validator.model; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Collection; + +import org.junit.Test; + +import help.AbstractHelpTest; +import help.validator.*; +import help.validator.location.DirectoryHelpModuleLocation; + +public class HelpFileTest extends AbstractHelpTest { + + @Test + public void testGoodHTML() throws IOException { + + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + + Path topic = createFakeHelpTopic(helpDir); + + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createGoodHTMLFile(topic); + new HelpFile(helpLocation, html); + + // if we get here, then no exceptions happened + } + + @Test + public void testBadHTML_InvalidStyleSheet() throws Exception { + + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + + Path topic = createFakeHelpTopic(helpDir); + + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createBadHTMLFile_InvalidStyleSheet(topic); + + try { + new HelpFile(helpLocation, html); + fail("Parsing did not fail for invalid stylesheet"); + } + catch (Exception e) { + // good + } + + } + + @Test + public void testBadHTML_InvalidAnchorRef_BadURI() throws Exception { + + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + + Path topic = createFakeHelpTopic(helpDir); + + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createBadHTMLFile_InvalidAnchor_BadURI(topic); + + try { + new HelpFile(helpLocation, html); + fail("Parsing did not fail for invalid stylesheet"); + } + catch (Exception e) { + // good + } + } + + @Test + public void testBadHTML_InvalidAnchorRef_WrongAttribtues() throws Exception { + // no 'name' or 'href' attribute + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + + Path topic = createFakeHelpTopic(helpDir); + + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createBadHTMLFile_InvalidAnchor_WrongAttributes(topic); + + try { + new HelpFile(helpLocation, html); + fail("Parsing did not fail for invalid stylesheet"); + } + catch (Exception e) { + // good + } + } + + @Test + public void testBadHTML_InvalidIMG_WrongAttribtues() throws Exception { + // no 'src' + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + + Path topic = createFakeHelpTopic(helpDir); + + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createBadHTMLFile_InvalidIMG_WrongAttributes(topic); + + try { + new HelpFile(helpLocation, html); + fail("Parsing did not fail for invalid stylesheet"); + } + catch (Exception e) { + // good + } + } + + @Test + public void testCommentGetsIgnored() throws Exception { + + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + Path topic = createFakeHelpTopic(helpDir); + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + + Path html = createGoodHTMLFile_InvalidAnchor_CommentedOut_MultiLineComment(topic); + HelpFile helpFile = new HelpFile(helpLocation, html); + Collection hrefs = helpFile.getAllHREFs(); + assertTrue(hrefs.isEmpty()); + } + + // @Test + // for debugging a real help file + public void test() throws Exception { + + Path path = Paths.get("//ghidra/Ghidra/Features/" + + "Base/src/main/help/help/topics/Annotations/Annotations.html"); + + Path helpDir = createTempHelpDir(); + addRequiredHelpDirStructure(helpDir); + DirectoryHelpModuleLocation helpLocation = + new DirectoryHelpModuleLocation(helpDir.toFile()); + AnchorManager anchorManager = new AnchorManager(); + ReferenceTagProcessor tagProcessor = new ReferenceTagProcessor(helpLocation, anchorManager); + HTMLFileParser.scanHtmlFile(path, tagProcessor); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + /** Has valid links */ + private Path createGoodHTMLFile(Path topic) throws IOException { + String anchor = "ManagePluginsDialog"; + return createHelpContent(topic, anchor); + } + + private Path createBadHTMLFile_InvalidAnchor_WrongAttributes(Path topic) throws IOException { + Path htmlPath = topic.resolve("FakeHTML_WrongAttributes.html"); + Path file = Files.createFile(htmlPath); + + String badAttr = "bob=1"; + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + "\n" + + "

Configure Tool

\n" + + "Some text with reference to shared image Click me\n" + + "\n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + private Path createBadHTMLFile_InvalidIMG_WrongAttributes(Path topic) throws IOException { + Path htmlPath = topic.resolve("FakeHTML_WrongAttributes.html"); + Path file = Files.createFile(htmlPath); + + String badAttr = "bob=1"; + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + "\n" + + "

Configure Tool

\n" + + "Some text with reference to shared image \n" + + "\n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + private Path createBadHTMLFile_InvalidAnchor_BadURI(Path topic) throws IOException { + Path htmlPath = topic.resolve("FakeHTML_BadURI.html"); + Path file = Files.createFile(htmlPath); + + String badURI = ":baduri"; // no scheme name on this URI + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + "\n" + + "

Configure Tool

\n" + + "Some text with reference to shared image Click me\n" + + "\n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + private Path createBadHTMLFile_InvalidStyleSheet(Path topic) throws IOException { + Path htmlPath = topic.resolve("FakeHTML_InvalidStyleSheet.html"); + Path file = Files.createFile(htmlPath); + + String badName = "bad_name"; + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + "\n" + + "

Configure Tool

\n" + + "Some text with reference to shared image \n" + + "\n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } + + private Path createGoodHTMLFile_InvalidAnchor_CommentedOut_MultiLineComment(Path topic) + throws IOException { + Path htmlPath = topic.resolve("HTMLWithComment.html"); + Path file = Files.createFile(htmlPath); + + String badURI = ":baduri"; // no scheme name on this URI + + //@formatter:off + String HTML = + "\n" + + "\n" + + "Configure Tool\n" + + "\n" + + "\n" + + "\n" + + "

Configure Tool

\n" + + " " + + "\n" + + "\n" + + "\n"; + //@formatter:on + + Files.write(file, HTML.getBytes(), StandardOpenOption.CREATE); + return file; + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessGraphicsEnvironment.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessGraphicsEnvironment.java new file mode 100644 index 0000000000..6424cf94ab --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessGraphicsEnvironment.java @@ -0,0 +1,94 @@ +/* ### + * IP: GHIDRA + * REVIEWED: YES + * + * 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.util.headless; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Locale; + +import sun.java2d.HeadlessGraphicsEnvironment; + +public class MyHeadlessGraphicsEnvironment extends GraphicsEnvironment { + + static volatile boolean swingErrorRegistered = false; + private static String preferredGraphicsEnv; + + private GraphicsEnvironment localEnv; + + static void setup() { + //System.setProperty("java.awt.headless", "true"); + preferredGraphicsEnv = System.getProperty("java.awt.graphicsenv"); + System.setProperty("java.awt.graphicsenv", MyHeadlessGraphicsEnvironment.class.getName()); + } + + public MyHeadlessGraphicsEnvironment() { + swingErrorRegistered = true; + try { + throw new Exception("Swing invocation detected for Headless Mode"); + } + catch (Exception e) { + e.printStackTrace(); + } + getRealGraphicsEnvironemnt(); + } + + @Override + public Graphics2D createGraphics(BufferedImage img) { + return null; + } + + @Override + public Font[] getAllFonts() { + return localEnv.getAllFonts(); + } + + @Override + public String[] getAvailableFontFamilyNames() { + return localEnv.getAvailableFontFamilyNames(); + } + + @Override + public String[] getAvailableFontFamilyNames(Locale l) { + return localEnv.getAvailableFontFamilyNames(l); + } + + @Override + public GraphicsDevice getDefaultScreenDevice() throws HeadlessException { + return localEnv.getDefaultScreenDevice(); + } + + @Override + public GraphicsDevice[] getScreenDevices() throws HeadlessException { + return localEnv.getScreenDevices(); + } + + private void getRealGraphicsEnvironemnt() { + try { + localEnv = (GraphicsEnvironment) Class.forName(preferredGraphicsEnv).newInstance(); + if (isHeadless()) { + localEnv = new HeadlessGraphicsEnvironment(localEnv); + } + } catch (ClassNotFoundException e) { + throw new Error("Could not find class: " + preferredGraphicsEnv); + } catch (InstantiationException e) { + throw new Error("Could not instantiate Graphics Environment: " + preferredGraphicsEnv); + } catch (IllegalAccessException e) { + throw new Error("Could not access Graphics Environment: " + preferredGraphicsEnv); + } + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessToolkit.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessToolkit.java new file mode 100644 index 0000000000..73b90e9ee0 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/util/headless/MyHeadlessToolkit.java @@ -0,0 +1,195 @@ +/* ### + * 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.util.headless; + +import java.awt.*; +import java.awt.Dialog.ModalExclusionType; +import java.awt.Dialog.ModalityType; +import java.awt.datatransfer.Clipboard; +import java.awt.font.TextAttribute; +import java.awt.im.InputMethodHighlight; +import java.awt.image.*; +import java.net.URL; +import java.util.Map; +import java.util.Properties; + +import sun.awt.HeadlessToolkit; + +public class MyHeadlessToolkit extends Toolkit { + + static volatile boolean swingErrorRegistered = false; + private static String preferredToolkit; + + private Toolkit localToolKit; + + static void setup() { + //System.setProperty("java.awt.headless", "true"); + preferredToolkit = System.getProperty("awt.toolkit", "sun.awt.X11.XToolkit"); + System.setProperty("awt.toolkit", MyHeadlessToolkit.class.getName()); + } + + public MyHeadlessToolkit() { + swingErrorRegistered = true; + try { + throw new Exception("Swing invocation detected for Headless Mode"); + } + catch (Exception e) { + e.printStackTrace(); + } + getRealToolkit(); + } + + @Override + public void beep() { + localToolKit.beep(); + } + + @Override + public int checkImage(Image image, int width, int height, + ImageObserver observer) { + return localToolKit.checkImage(image, width, height, observer); + } + + @Override + public Image createImage(String filename) { + return localToolKit.createImage(filename); + } + + @Override + public Image createImage(URL url) { + return localToolKit.createImage(url); + } + + @Override + public Image createImage(ImageProducer producer) { + return localToolKit.createImage(producer); + } + + @Override + public Image createImage(byte[] imagedata, int imageoffset, int imagelength) { + return localToolKit.createImage(imagedata, imageoffset, imagelength); + } + + @Override + public ColorModel getColorModel() throws HeadlessException { + return localToolKit.getColorModel(); + } + + @Override + public String[] getFontList() { + return localToolKit.getFontList(); + } + + @Override + public FontMetrics getFontMetrics(Font font) { + return localToolKit.getFontMetrics(font); + } + + @Override + public Image getImage(String filename) { + return localToolKit.getImage(filename); + } + + @Override + public Image getImage(URL url) { + return localToolKit.getImage(url); + } + + @Override + public PrintJob getPrintJob(Frame frame, String jobtitle, Properties props) { + return localToolKit.getPrintJob(frame, jobtitle, props); + } + + @Override + public int getScreenResolution() throws HeadlessException { + return localToolKit.getScreenResolution(); + } + + @Override + public Dimension getScreenSize() throws HeadlessException { + return localToolKit.getScreenSize(); + } + + @Override + public Clipboard getSystemClipboard() throws HeadlessException { + return localToolKit.getSystemClipboard(); + } + + @Override + protected EventQueue getSystemEventQueueImpl() { + return localToolKit.getSystemEventQueue(); + } + + @Override + public boolean isModalExclusionTypeSupported( + ModalExclusionType modalExclusionType) { + return localToolKit.isModalExclusionTypeSupported(modalExclusionType); + } + + @Override + public boolean isModalityTypeSupported(ModalityType modalityType) { + return localToolKit.isModalityTypeSupported(modalityType); + } + + @Override + public Map mapInputMethodHighlight( + InputMethodHighlight highlight) throws HeadlessException { + return localToolKit.mapInputMethodHighlight(highlight); + } + + @Override + public boolean prepareImage(Image image, int width, int height, + ImageObserver observer) { + return localToolKit.prepareImage(image, width, height, observer); + } + + @Override + public void sync() { + localToolKit.sync(); + } + + private void getRealToolkit() { + try { + // We disable the JIT during toolkit initialization. This + // tends to touch lots of classes that aren't needed again + // later and therefore JITing is counter-productiive. + java.lang.Compiler.disable(); + + Class cls = null; + try { + try { + cls = Class.forName(preferredToolkit); + } catch (ClassNotFoundException ee) { + throw new AWTError("Toolkit not found: " + preferredToolkit); + } + if (cls != null) { + localToolKit = (Toolkit)cls.newInstance(); + if (GraphicsEnvironment.isHeadless()) { + localToolKit = new HeadlessToolkit(localToolKit); + } + } + } catch (InstantiationException e) { + throw new AWTError("Could not instantiate Toolkit: " + preferredToolkit); + } catch (IllegalAccessException e) { + throw new AWTError("Could not access Toolkit: " + preferredToolkit); + } + + } finally { + // Make sure to always re-enable the JIT. + java.lang.Compiler.enable(); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/PluginManagerTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/PluginManagerTest.java new file mode 100644 index 0000000000..62abc0b1eb --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/PluginManagerTest.java @@ -0,0 +1,313 @@ +/* ### + * 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.framework.plugintool; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.stream.Collectors; + +import org.junit.*; + +import ghidra.framework.plugintool.testplugins.*; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.framework.plugintool.util.PluginUtils; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; +import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.exception.AssertException; + +public class PluginManagerTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private PluginTool tool; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + tool = env.getTool(); + runSwing(() -> tool.setConfigChanged(false)); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + @Test + public void testConflictPluginNames() { + // Test loading a plugin that has a name conflict with another plugin + try { + tool.addPlugin(NameConflictingPlugin.class.getName()); + fail("Should have gotten a plugin exception because of the conflicting plugin names"); + } + catch (PluginException e) { + // ensure that the exception message mentions both of the conflicting plugin class names. + String msg = e.getMessage(); + assertTrue(msg.contains(NameConflictingPlugin.class.getName())); + assertTrue(msg.contains( + ghidra.framework.plugintool.testplugins.secondconflict.NameConflictingPlugin.class.getName())); + } + assertNotPlugin(NameConflictingPlugin.class); + assertNotPlugin( + ghidra.framework.plugintool.testplugins.secondconflict.NameConflictingPlugin.class); + } + + @Test + public void testDiamond() throws PluginException { + // Test loading a plugin (A) that has dependencies in a diamond shape. + // A + // / \ + // B C + // \ / + // D + tool.addPlugin(DiamondPluginA.class.getName()); + + assertPlugin(DiamondPluginA.class); + assertPlugin(DiamondPluginB.class); + assertPlugin(DiamondPluginC.class); + assertPlugin(DiamondPluginD.class); + assertTrue(tool.hasConfigChanged()); + } + + @Test + public void testCircularDependency() throws PluginException { + // Test loading plugins that have circular dependencies. + // Currently this is allowed but a warning is logged to the message console. + // +---------+ + // | | + // v | + // A ------> B + tool.addPlugin(CircularPluginA.class.getName()); + + assertPlugin(CircularPluginA.class); + assertPlugin(CircularPluginB.class); + assertTrue(tool.hasConfigChanged()); + + removePlugin(getPlugin(CircularPluginB.class), false); + + assertNotPlugin(CircularPluginA.class); + assertNotPlugin(CircularPluginB.class); + } + + @Test + public void testLoadSameMultipleTimes() throws PluginException { + // Loading the same plugin multiple times should not cause a problem and the second + // attempt should just be ignored. + tool.addPlugin(DiamondPluginD.class.getName()); + assertPlugin(DiamondPluginD.class); + assertTrue(tool.hasConfigChanged()); + + tool.addPlugin(DiamondPluginD.class.getName()); + // as long as we didn't get an exception everything is good + } + + @Test + public void testMissingDependency() throws PluginException { + // Test loading a plugin that has an dependency without a default provider. + int pluginCount = tool.getManagedPlugins().size(); + try { + tool.addPlugin(MissingDepPluginA.class.getName()); + fail("PluginA should have failed to load"); + } + catch (PluginException e) { + String msg = e.getMessage(); + assertTrue(msg.contains("Unresolved dependency") && + msg.contains(MissingDepServiceB.class.getName())); + } + assertEquals(pluginCount, tool.getManagedPlugins().size()); + assertTrue(tool.hasConfigChanged()); + + // If we manually add the missing dependency, it should now work + tool.addPlugin(MissingDepPluginB.class.getName()); + tool.addPlugin(MissingDepPluginA.class.getName()); + + assertPlugin(MissingDepPluginB.class); + assertPlugin(MissingDepPluginA.class); + assertTrue(tool.hasConfigChanged()); + } + + @Test + public void testLoadingDepSimultaneously() throws PluginException { + // Load a plugin and a second plugin that provides a dependency at the same time. + // Loading PluginA by itself would fail. + tool.addPlugins( + new String[] { MissingDepPluginA.class.getName(), MissingDepPluginB.class.getName() }); + + assertPlugin(MissingDepPluginA.class); + assertPlugin(MissingDepPluginB.class); + assertTrue(tool.hasConfigChanged()); + } + + @Test + public void testInitFail() { + // Test that a plugin that throws an error during init() is removed and cleaned up. + int pluginCount = tool.getManagedPlugins().size(); + int disposeCountB = InitFailPluginB.disposeCount; + + setErrorsExpected(true); + try { + tool.addPlugin(InitFailPluginB.class.getName()); + fail( + "PluginB should have failed to load because PluginB throws exception during init()"); + } + catch (PluginException pe) { + // good + assertTrue(pe.getMessage().contains(InitFailPluginB.ERROR_MSG)); + } + setErrorsExpected(false); + + assertEquals(disposeCountB + 1, InitFailPluginB.disposeCount); + assertEquals(pluginCount, tool.getManagedPlugins().size()); + assertTrue(tool.hasConfigChanged()); + } + + @Test + public void testInitFailInDependency() { + // Test that a plugin that has a dependency that throws an error during init() + // is removed and cleaned up. + int pluginCount = tool.getManagedPlugins().size(); + int disposeCountA = InitFailPluginA.disposeCount; + int disposeCountB = InitFailPluginB.disposeCount; + + setErrorsExpected(true); + try { + tool.addPlugin(InitFailPluginA.class.getName()); + fail( + "PluginA should have failed to load because PluginB throws exception during init()"); + } + catch (PluginException pe) { + // good + assertTrue(pe.getMessage().contains(InitFailPluginB.ERROR_MSG)); + } + setErrorsExpected(false); + + assertNotPlugin(InitFailPluginA.class); + assertEquals(pluginCount, tool.getManagedPlugins().size()); + assertEquals(disposeCountB + 1, InitFailPluginB.disposeCount); + assertEquals(disposeCountA + 1, InitFailPluginA.disposeCount); + } + + @Test + public void testDisposeFail() throws PluginException { + // Test when a plugin throws an exception during its dispose() + // Exceptions are logged to the console because the dispose was executed + // on the swing thread. + tool.addPlugin(DisposeFailPluginA.class.getName()); + assertTrue(tool.hasConfigChanged()); + Plugin pluginA = getPlugin(DisposeFailPluginA.class); + + runSwing(() -> tool.setConfigChanged(false)); + + removePlugin(pluginA, true); + + assertNotPlugin(DisposeFailPluginA.class); + assertTrue(tool.hasConfigChanged()); + } + + @Test + public void testLoadFailure_Isolated() { + // test loading multiple plugins, good should load successfully, bad ones + // shouldn't cause entire failure. + + setErrorsExpected(true); + try { + tool.addPlugins(new String[] { IsolatedFailPluginA.class.getName(), + IsolatedFailPluginB.class.getName() }); + fail("Should have gotten an exception because PluginB was bad"); + } + catch (PluginException pe) { + // currently expected behavior, partial errors cause exception + } + setErrorsExpected(false); + + assertTrue(tool.hasConfigChanged()); + assertPlugin(IsolatedFailPluginA.class); + assertNotPlugin(IsolatedFailPluginB.class); + } + + @Test + public void testUniquePluginNames() { + // Ensure all current plugins have a unique name. + // Build a string with a list of the bad plugins and report it at the end. + + Map> simpleNameToClassMap = new HashMap<>(); + Map>> badPlugins = new HashMap<>(); + + for (Class pluginClass : ClassSearcher.getClasses(Plugin.class)) { + if (TestingPlugin.class.isAssignableFrom(pluginClass)) { + // ignore plugins marked with the TestingPlugin interface because + // they are supposed to have name conflicts and other problems. + continue; + } + String pluginName = PluginUtils.getPluginNameFromClass(pluginClass); + Class previousPluginClass = + simpleNameToClassMap.putIfAbsent(pluginName, pluginClass); + if (previousPluginClass != null) { + Set> set = + badPlugins.computeIfAbsent(pluginName, x -> new HashSet<>()); + set.add(pluginClass); + set.add(previousPluginClass); + } + } + + // simplename: pluginclassname1,pluginclassname2*\n[repeat]. + String badPluginsStr = badPlugins.entrySet().stream().map( // + entry -> entry.getKey() + ": " + + entry.getValue().stream().map(pc -> pc.getName()).collect(Collectors.joining(",")) // + ).collect(Collectors.joining("\n")); + + assertTrue("Plugins with name collisions: " + badPluginsStr, badPluginsStr.isEmpty()); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void removePlugin(Plugin p, boolean exceptional) { + + runSwing(() -> { + try { + + tool.removePlugins(new Plugin[] { p }); + } + catch (Exception e) { + if (!exceptional) { + throw new AssertException(e); + } + } + }); + } + + private void assertPlugin(Class pluginClass) { + assertTrue("Plugin " + pluginClass.getName() + " not present", + tool.getManagedPlugins().stream().anyMatch(p -> p.getClass() == pluginClass)); + } + + private void assertNotPlugin(Class pluginClass) { + + List plugins = tool.getManagedPlugins(); + for (Plugin p : plugins) { + assertNotSame("Plugin " + pluginClass.getName() + " loaded but shouldn't be", + p.getClass(), pluginClass); + } + } + + private Plugin getPlugin(Class pluginClass) { + return tool.getManagedPlugins().stream().filter( + p -> p.getClass() == pluginClass).findFirst().get(); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/TestingPlugin.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/TestingPlugin.java new file mode 100644 index 0000000000..eb55d5e467 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/TestingPlugin.java @@ -0,0 +1,23 @@ +/* ### + * 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.framework.plugintool; + +/** + * Marker interface to signal that the implementing class is a test plugin and should + * not be considered as 'real'. + */ +public interface TestingPlugin { +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginA.java new file mode 100644 index 0000000000..9adced6e2d --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginA.java @@ -0,0 +1,40 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** +* Plugin for circular dependency testing {@link PluginManagerTest#testCircularDependency()} +*/ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = { CircularServiceA.class }, + servicesRequired = { CircularServiceB.class } ) +//@formatter:on +public class CircularPluginA extends Plugin implements CircularServiceA, TestingPlugin { + + public CircularPluginA(PluginTool tool) { + super(tool); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginB.java new file mode 100644 index 0000000000..69da0edca5 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularPluginB.java @@ -0,0 +1,40 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** +* Plugin for circular dependency testing {@link PluginManagerTest#testCircularDependency()} +*/ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = CircularServiceB.class, + servicesRequired = CircularServiceA.class) +//@formatter:on +public class CircularPluginB extends Plugin implements CircularServiceB, TestingPlugin { + + public CircularPluginB(PluginTool tool) { + super(tool); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceA.java new file mode 100644 index 0000000000..58eb36cdcc --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceA.java @@ -0,0 +1,27 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** +* Plugin service for circular dependency testing {@link PluginManagerTest#testCircularDependency()} +*/ +@ServiceInfo(defaultProvider = CircularPluginA.class, description = "Test service") +public interface CircularServiceA { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceB.java new file mode 100644 index 0000000000..968fe9f0a6 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/CircularServiceB.java @@ -0,0 +1,27 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** +* Plugin service for circular dependency testing {@link PluginManagerTest#testCircularDependency()} +*/ +@ServiceInfo(defaultProvider = CircularPluginB.class, description = "Test service") +public interface CircularServiceB { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginA.java new file mode 100644 index 0000000000..60c6c91d56 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginA.java @@ -0,0 +1,47 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testDiamond()} + *
+ *      A <------- you are here
+ *    /   \
+ *   B     C
+ *    \   /
+ *      D
+ * 
+ */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesRequired = { DiamondServiceB.class, DiamondServiceC.class} ) +//@formatter:on +public class DiamondPluginA extends Plugin implements TestingPlugin { + + public DiamondPluginA(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginB.java new file mode 100644 index 0000000000..14424c1031 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginB.java @@ -0,0 +1,48 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B <---C--------------- you are here
+ *    \   /
+ *      D
+ * 
+ */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = DiamondServiceB.class, + servicesRequired = DiamondServiceD.class) +//@formatter:on +public class DiamondPluginB extends Plugin implements DiamondServiceB, TestingPlugin { + + public DiamondPluginB(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginC.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginC.java new file mode 100644 index 0000000000..f8a86abbfa --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginC.java @@ -0,0 +1,48 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B     C <------ you are here
+ *    \   /
+ *      D
+ * 
+ */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = DiamondServiceC.class, + servicesRequired = DiamondServiceD.class) +//@formatter:on +public class DiamondPluginC extends Plugin implements DiamondServiceC, TestingPlugin { + + public DiamondPluginC(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginD.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginD.java new file mode 100644 index 0000000000..c121c9cb48 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondPluginD.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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B     C
+ *    \   /
+ *      D <--------- you are here
+ * 
+ */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = DiamondServiceD.class) +//@formatter:on +public class DiamondPluginD extends Plugin implements DiamondServiceD, TestingPlugin { + + public DiamondPluginD(PluginTool tool) { + super(tool); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceB.java new file mode 100644 index 0000000000..77647df019 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceB.java @@ -0,0 +1,34 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * Test plugin service for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B<----C------------ you are here
+ *    \   /
+ *      D
+ * 
+ */ +@ServiceInfo(defaultProvider = DiamondPluginB.class, description = "Test service") +public interface DiamondServiceB { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceC.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceC.java new file mode 100644 index 0000000000..489ff78409 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceC.java @@ -0,0 +1,34 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * Test plugin service for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B     C <---------- you are here
+ *    \   /
+ *      D
+ * 
+ */ +@ServiceInfo(defaultProvider = DiamondPluginC.class, description = "Test service") +public interface DiamondServiceC { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceD.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceD.java new file mode 100644 index 0000000000..0f6427ec97 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DiamondServiceD.java @@ -0,0 +1,34 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * Test plugin service for {@link PluginManagerTest#testDiamond()} + *
+ *      A
+ *    /   \
+ *   B     C
+ *    \   /
+ *      D <---------- you are here
+ * 
+ */ +@ServiceInfo(defaultProvider = DiamondPluginD.class, description = "Test service") +public interface DiamondServiceD { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DisposeFailPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DisposeFailPluginA.java new file mode 100644 index 0000000000..cab4780550 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/DisposeFailPluginA.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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testDisposeFail()} and friends. + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin" ) +//@formatter:on +public class DisposeFailPluginA extends Plugin implements TestingPlugin { + public static final String ERROR_MSG = "PluginA dispose exception message"; + public static int disposeCount = 0; + + public DisposeFailPluginA(PluginTool tool) { + super(tool); + } + + @Override + protected void dispose() { + throw new RuntimeException(ERROR_MSG); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginA.java new file mode 100644 index 0000000000..a238505872 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginA.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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testInitFail()} and friends. + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesRequired = { InitFailServiceB.class } ) +//@formatter:on +public class InitFailPluginA extends Plugin implements TestingPlugin { + public static int disposeCount = 0; + + public InitFailPluginA(PluginTool tool) { + super(tool); + } + + @Override + protected void dispose() { + disposeCount++; + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginB.java new file mode 100644 index 0000000000..44d5c4e081 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailPluginB.java @@ -0,0 +1,52 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testInitFail()} and friends. + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = { InitFailServiceB.class } ) +//@formatter:on +public class InitFailPluginB extends Plugin implements InitFailServiceB, TestingPlugin { + public static final String ERROR_MSG = "PluginB fails during Plugin.init()"; + public static int disposeCount = 0; + + public InitFailPluginB(PluginTool tool) { + super(tool); + } + + @Override + protected void init() { + throw new RuntimeException(ERROR_MSG); + } + + @Override + protected void dispose() { + disposeCount++; + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailServiceB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailServiceB.java new file mode 100644 index 0000000000..84323d0314 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/InitFailServiceB.java @@ -0,0 +1,27 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * Test plugin service for {@link PluginManagerTest#testInitFail()} and friends. + */ +@ServiceInfo(defaultProvider = InitFailPluginB.class, description = "Test service") +public interface InitFailServiceB { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginA.java new file mode 100644 index 0000000000..b701c7e984 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginA.java @@ -0,0 +1,39 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testLoadFailure_Isolated} + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin") +//@formatter:on +public class IsolatedFailPluginA extends Plugin implements TestingPlugin { + + public IsolatedFailPluginA(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginB.java new file mode 100644 index 0000000000..459bf4c102 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/IsolatedFailPluginB.java @@ -0,0 +1,45 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testLoadFailure_Isolated} + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin") +//@formatter:on +public class IsolatedFailPluginB extends Plugin implements TestingPlugin { + + public static final String ERROR_MSG = "IsolatedFailPluginB error message"; + + public IsolatedFailPluginB(PluginTool tool) { + super(tool); + } + + @Override + protected void init() { + throw new RuntimeException(ERROR_MSG); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginA.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginA.java new file mode 100644 index 0000000000..0965283993 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginA.java @@ -0,0 +1,41 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testMissingDependency()} and + * {@link PluginManagerTest#testLoadingDepSimultaneously()} + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesRequired = { MissingDepServiceB.class } ) +//@formatter:on +public class MissingDepPluginA extends Plugin implements TestingPlugin { + + public MissingDepPluginA(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginB.java new file mode 100644 index 0000000000..bd94be8b07 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepPluginB.java @@ -0,0 +1,41 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testMissingDependency()} and + * {@link PluginManagerTest#testLoadingDepSimultaneously()} + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin", + description = "Test plugin", + servicesProvided = MissingDepServiceB.class) +//@formatter:on +public class MissingDepPluginB extends Plugin implements MissingDepServiceB, TestingPlugin { + + public MissingDepPluginB(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepServiceB.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepServiceB.java new file mode 100644 index 0000000000..14723e09f7 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/MissingDepServiceB.java @@ -0,0 +1,28 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.framework.plugintool.PluginManagerTest; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * Test plugin service for {@link PluginManagerTest#testMissingDependency()} and + * {@link PluginManagerTest#testLoadingDepSimultaneously()} + */ +@ServiceInfo(description = "Test service" /*, defaultProvider=not_specified*/ ) +public interface MissingDepServiceB { + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/NameConflictingPlugin.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/NameConflictingPlugin.java new file mode 100644 index 0000000000..ff97a0ff06 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/NameConflictingPlugin.java @@ -0,0 +1,41 @@ +/* ### + * 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.framework.plugintool.testplugins; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +//@formatter:off +/** + * Test plugin for {@link PluginManagerTest#testConflictPluginNames()}. + * + * Class name needs to match *Plugin so the class searcher finds it. + */ +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin name collision", + description = "Test plugin name collision.") +//@formatter:on +public class NameConflictingPlugin extends Plugin implements TestingPlugin { + + public NameConflictingPlugin(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/secondconflict/NameConflictingPlugin.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/secondconflict/NameConflictingPlugin.java new file mode 100644 index 0000000000..f30df9b5d3 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/plugintool/testplugins/secondconflict/NameConflictingPlugin.java @@ -0,0 +1,41 @@ +/* ### + * 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.framework.plugintool.testplugins.secondconflict; + +import ghidra.app.DeveloperPluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +/** + * Test plugin for {@link PluginManagerTest#testConflictPluginNames()} + * + * Class name needs to match *Plugin so the class searcher finds it. + */ +//@formatter:off +@PluginInfo(status = PluginStatus.HIDDEN, + packageName = DeveloperPluginPackage.NAME, + category = PluginCategoryNames.UNMANAGED, + shortDescription = "Test plugin name collision", + description = "Test plugin name collision.") +//@formatter:on +public class NameConflictingPlugin extends Plugin implements TestingPlugin { + + public NameConflictingPlugin(PluginTool tool) { + super(tool); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/disassemble/DisassemblerTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/disassemble/DisassemblerTest.java new file mode 100644 index 0000000000..3428ddd822 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/disassemble/DisassemblerTest.java @@ -0,0 +1,1534 @@ +/* ### + * 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.program.disassemble; + +import static org.junit.Assert.*; + +import java.math.BigInteger; +import java.util.List; +import java.util.Set; + +import org.junit.*; + +import ghidra.program.model.address.*; +import ghidra.program.model.data.ByteDataType; +import ghidra.program.model.lang.Register; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.SourceType; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.test.ToyProgramBuilder; +import ghidra.util.task.TaskMonitorAdapter; +import util.CollectionUtils; + +public class DisassemblerTest extends AbstractGhidraHeadlessIntegrationTest { + + // TODO: Disassembler Concerns + // - Unsure we can detect context inconsistencies where context produces constant and + // does not influence parse decision (i.e., prototypes are equal) + // - Once CodeManager detects block overlap it stops processing instructions within the + // block at the point of duplication causing inconsistent prototypes not to be detected + // in some cases (see testDisassemblerMidBlockOverlapWithProgramAndConflictDetection) + // - InstructionBlock holds only one conflict - this may cause + // some conflicts to get lost - CodeManager also sets conflicts which + // can wipe a previous conflict + // - Conflicting context setting (via globalset) are not detected - last one wins + // - We are inconsistent in our treatment of uninitialized memory and undefined memory + // in terms of conflict/error bookmarks + + // TODO: Change error handling of unintialized memory and EXTERNAL block + + // TODO: Add test where internal conflict occurs on top of different program + // code units - CODE_UNIT conflict handling assumes real code unit exists at conflictAddress + + private ToyProgramBuilder programBuilder;// Instructions are 2-byte aligned + private Program program; + private Listing listing; + private Disassembler disassembler; + + private int txId; + + @Before + public void setUp() throws Exception { + programBuilder = new ToyProgramBuilder("Test", true, true, null); + program = programBuilder.getProgram(); + txId = program.startTransaction("Add Memory");// leave open until tearDown + programBuilder.createMemory(".text", "0", 64).setExecute(true);// initialized + programBuilder.createUninitializedMemory(".unint", "0x40", 64).setExecute(true);// uninitialized + programBuilder.createUninitializedMemory(".dat", "0x80", 64);// no-execute + programBuilder.createMemory(".text2", "0x3e0", 0x800).setExecute(true);// initialized + + listing = program.getListing(); + disassembler = new Disassembler(program, TaskMonitorAdapter.DUMMY_MONITOR, null); + } + + @After + public void tearDown() throws Exception { + if (program != null) { + program.endTransaction(txId, true); + } + if (programBuilder != null) { + programBuilder.dispose(); + } + } + + /** + * ** Diagnostic Aid ** Dump context register values + */ +// private void dumpContextRanges(long min, long max, String regName) { +// Msg.info(this, "Context ranges: " + regName); +// Register fctxReg = program.getRegister(regName); +// ProgramContext programContext = program.getProgramContext(); +// AddressRangeIterator ranges = +// programContext.getRegisterValueAddressRanges(fctxReg, addr(min), addr(max)); +// for (AddressRange range : ranges) { +// Msg.info(this, +// range + ": " + programContext.getValue(fctxReg, range.getMinAddress(), false)); +// } +// assertFalse(ranges.hasNext()); +// } + + private Address addr(long offset) { + return programBuilder.getAddress(offset); + } + + private AddressRange range(long start, long end) { + return new AddressRangeImpl(addr(start), addr(end)); + } + + private AddressSet addrset(AddressRange... ranges) { + AddressSet addrset = new AddressSet(); + for (AddressRange range : ranges) { + addrset.add(range); + } + return addrset; + } + + private void verifyInstructionPresence() { + verifyInstructionPresence(null); + } + + private void verifyInstructionPresence(Set
exclusions) { +// InstructionIterator instructions = listing.getInstructions(true); +// while (instructions.hasNext()) { +// Instruction instr = instructions.next(); +// System.out.println("Instruction at " + instr.getAddress()); +// } + List
instrAddrs = programBuilder.getDefinedInstructionAddress(); + int expectedCnt = instrAddrs.size(); + if (exclusions != null) { + expectedCnt -= exclusions.size(); + } + assertEquals(expectedCnt, listing.getNumInstructions()); + for (Address addr : instrAddrs) { + if (exclusions != null && exclusions.contains(addr)) { + continue; + } + assertNotNull("Expected instruction at " + addr, listing.getInstructionAt(addr)); + } + } + + private void verifyNoBookmarks() { + assertEquals("unexpected bookmarks exist", 0, + program.getBookmarkManager().getBookmarkCount()); + } + + private void verifyErrorBookmark(Address addr, String text) { + assertEquals("Expected error bookmark at " + addr, 1, + program.getBookmarkManager().getBookmarkCount()); + + Bookmark errMark = program.getBookmarkManager().getBookmark(addr, BookmarkType.ERROR, + Disassembler.ERROR_BOOKMARK_CATEGORY); + assertNotNull("Expected error bookmark at " + addr, errMark); + + if (text != null) { + assertTrue("Expected error bookmark at " + addr + " to contain text: " + text, + errMark.getComment().indexOf(text) >= 0); + } + + } + + private static class ContextRangeValue { + long startOffset; + long endOffset; + int value; + + ContextRangeValue(long start, long end, int value) { + startOffset = start; + endOffset = end; + this.value = value; + } + } + + private void verifyContextRanges(long min, long max, String regName, + ContextRangeValue... valueRanges) { + Register fctxReg = program.getRegister(regName); + ProgramContext programContext = program.getProgramContext(); + AddressRangeIterator ranges = + programContext.getRegisterValueAddressRanges(fctxReg, addr(min), addr(max)); + for (ContextRangeValue valueRange : valueRanges) { + assertTrue(ranges.hasNext()); + AddressRange range = ranges.next(); + assertEquals(valueRange.startOffset, range.getMinAddress().getOffset()); + assertEquals(valueRange.endOffset, range.getMaxAddress().getOffset()); + BigInteger val = programContext.getValue(fctxReg, range.getMinAddress(), false); + assertNotNull("Expected flow context at " + range.getMinAddress()); + assertEquals(valueRange.value, val.longValue()); + } + assertFalse(ranges.hasNext()); + } + + /** + * + * +-> 10: breq 20 --+ (start) + * | 12: ret | + * | | + * +-- 20: breq 10 <-+ + * 22: ret + * + * Test circular flow + * + */ + @Test + public void testDisassemblerCircularFlow() throws Exception { + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesReturn(12); + + programBuilder.addBytesBranchConditional(20, 10); + programBuilder.addBytesReturn(22); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 13), range(20, 23)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * + * 10: call 20 --+ (start) + * 12: ret | + * | + * 20: or <----+ + * 22: ret + * + * Test call + * + */ + @Test + public void testDisassemblerCallFlow() throws Exception { + + programBuilder.addBytesCall(10, 20); + programBuilder.addBytesReturn(12); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesReturn(22); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 13), range(20, 23)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * + * 10: call 20 --+ (start) + * 12: ret | (should not disassemble) + * | + * 20: or <----+ (no-return) + * 22: ret + * + * Test call + * + */ + @Test + public void testDisassemblerCallFlowNoReturn() throws Exception { + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesReturn(22); + + AddressSetView disAddrs = disassembler.disassemble(addr(20), null); + AddressSet funcBody = addrset(range(20, 23)); + assertEquals(funcBody, disAddrs); + + verifyInstructionPresence(); + + Function func = listing.createFunction("Foo", addr(20), funcBody, SourceType.USER_DEFINED); + func.setNoReturn(true); + + programBuilder.addBytesCall(10, 20); + programBuilder.addBytesReturn(12);// should not disassemble + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(12))); + + verifyNoBookmarks(); + + } + + /** + * +--10: breq 20 (start) + * | 12: call 30 --+ + * | 14: ret | + * | | + * +-> 20: or | + * +-- 22: bral 40 | + * | | + * | 30: or <----+ + * | 32: bral 40 -+ + * | | + * +->40: ret <---+ + * + */ + @Test + public void testDisassemblerMultipath() throws Exception { + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesCall(12, 30); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesBranch(22, 40); + + programBuilder.addBytesFallthrough(30); + programBuilder.addBytesBranch(32, 40); + + programBuilder.addBytesReturn(40); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 23), range(30, 33), range(40, 41)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * + * 10: callds 20 --+ (start) + * 12: _or | + * 14: ret | + * | + * 20: or <------+ + * 22: ret + * + * Test simple delay slot flow + * + */ + @Test + public void testDisassemblerDelaySlot() throws Exception { + + programBuilder.addBytesCallWithDelaySlot(10, 20); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesReturn(22); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 23)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * 10: or (start) + * +- 12: brds 20 + * | 14: _or <-----+ + * | 16: ret | + * | | + * +->20: or | + * 22: breq 14 ----+ + * 24: ret + * + * Test branch into delay slot already in InstructionSet + * + */ + @Test + public void testDisassemblerBranchIntoDelaySlot() throws Exception { + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesBranchWithDelaySlot(12, 20); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesBranchConditional(22, 14); + programBuilder.addBytesReturn(24); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 17), range(20, 25)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(14)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * 4: bral 14 ----+ (start2) + * | + * 10: or | (start1) + * +- 12: brds 20 | + * | 14: _or <-----+ + * | 16: ret (part of second run) + * | + * +->20: ret + * + * Test branch into delay slot already in Program + * + */ + @Test + public void testDisassemblerBranchIntoDelaySlotInProgram() throws Exception { + + programBuilder.addBytesBranch(4, 14); + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesBranchWithDelaySlot(12, 20); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesReturn(20); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 21)), disAddrs); + + disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 5), range(16, 17)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(14)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * +-- 6: bral 12 (start) + * | + * | 10: callds 30 <-+ -+ + * +-> 12: _or | | + * 14: breq 20 --+ | | + * 16: ret | | | + * | | | + * 20: or <----+ | | + * 22: bral 10 ----+ | + * | + * 30: ret <---------+ + * + * Test delay slot disassembled first + * + */ + @Test + public void testDisassemblerDelaySlotFirst() throws Exception { + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesCallWithDelaySlot(10, 30); + programBuilder.addBytesBranchConditional(14, 20); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesBranch(22, 10); + + programBuilder.addBytesReturn(30); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(10, 17), range(20, 23), range(30, 31)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(10)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(12)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * +-- 6: bral 12 (start1) + * | + * | 10: callds 20 <-+ (start2) + * +-> 12: or 12: _or | + * 14: ret | + * | + * 20: ret <------+ + * + * Test delay slot disassembled first in program + * + */ + @Test + public void testDisassemblerDelaySlotFirstInProgram() throws Exception { + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesCallWithDelaySlot(10, 20); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesReturn(20); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(12, 15)), disAddrs); + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 13), range(20, 21)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(10)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(12)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * 4: fctx #2 (start) + * +-- 6: bral 14 + * | + * | 10: nfctx #3 <-+ + * | 12: callds 30 | --+ + * +-> 14: _or | | + * 16: breq 10 ----+ | + * 18: ret | + * | + * 30: ret <-----------+ + * + * Test delay slot disassembled first w/ context + * + */ + @Test + public void testDisassemblerDelaySlotFirstWithContext() throws Exception { + + programBuilder.addBytesFallthroughSetFlowContext(4, 2); + programBuilder.addBytesBranch(6, 14); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCallWithDelaySlot(12, 30); + programBuilder.addBytesBranchConditional(16, 10); + programBuilder.addBytesReturn(18); + + programBuilder.addBytesReturn(30); + + AddressSetView disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 7), range(10, 19), range(30, 31)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(14)); + assertTrue(instr.isInDelaySlot()); + + //dumpContextRanges(0, 40, "fctx"); + //dumpContextRanges(0, 40, "nfctx"); + + //openProgramInTool(); + + //@formatter:off + verifyContextRanges(0, 40, "fctx", + new ContextRangeValue(6, 7, 2), + new ContextRangeValue(10, 11, 2), + new ContextRangeValue(12, 12, 2), // range split due to nfctx set @ 12 + new ContextRangeValue(13, 19, 2), + new ContextRangeValue(30, 31, 2)); + verifyContextRanges(0, 40, "nfctx", + new ContextRangeValue(12, 12, 3)); + //@formatter:on + + } + + /** + * 4: fctx #2 (start) + * +-- 6: bral 14 + * | + * | 10: nfctx #3 <-+ + * | 12: call 30 | --+ + * +-> 14: breq 10 ----+ | + * 16: ret | + * | + * 30: ret <-----------+ + * + * Test disassembly w/ context (without delay slot) + * + */ + @Test + public void testDisassemblerWithContext() throws Exception { + + programBuilder.addBytesFallthroughSetFlowContext(4, 2); + programBuilder.addBytesBranch(6, 14); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCall(12, 30); + programBuilder.addBytesBranchConditional(14, 10); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesReturn(30); + + AddressSetView disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 7), range(10, 17), range(30, 31)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + //@formatter:off + verifyContextRanges(0, 40, "fctx", + new ContextRangeValue(6, 7, 2), + new ContextRangeValue(10, 11, 2), + new ContextRangeValue(12, 12, 2), // range split due to nfctx set @ 12 + new ContextRangeValue(13, 17, 2), + new ContextRangeValue(30, 31, 2)); + verifyContextRanges(0, 40, "nfctx", + new ContextRangeValue(12, 12, 3)); + //@formatter:on + + } + + /** + * 10: nfctx #3 + * 12: cop3 + * 14: ret + * + * Test use of non-flow context in disassembly + * + */ + @Test + public void testDisassemblerNonFlowContextParse() throws Exception { + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + //@formatter:off + verifyContextRanges(0, 40, "nfctx", + new ContextRangeValue(12, 12, 3)); + //@formatter:on + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals("cop3", instr.getMnemonicString()); + } + + /** + * 10: or + * 12: cop3 + * 14: ret + * + * Test use of non-flow context in disassembly (previously set) + * + */ + @Test + public void testDisassemblerNonFlowContextParsePreset() throws Exception { + + program.getProgramContext().setValue(program.getRegister("nfctx"), addr(12), addr(12), + BigInteger.valueOf(3)); + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals("cop3", instr.getMnemonicString()); + } + + /** + * 10: or + * 12: cop3 + * 14: ret + * + * Test use of non-flow context in disassembly (previously set - mid-range) + * + */ + @Test + public void testDisassemblerNonFlowContextParsePresetRange() throws Exception { + + program.getProgramContext().setValue(program.getRegister("nfctx"), addr(10), addr(12), + BigInteger.valueOf(3)); + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(12)); + assertEquals("cop3", instr.getMnemonicString()); + } + + // + // TEST RESTRICTED DISASSEMBLY + // + + /** + * + * 10: call 30 ----+ (start) + * 12: breq 20 --+ | + * 14: ret | | + * +-> 16: ret | | (should not disassemble) + * | ^^ restrict ^^ | | + * | | | + * +-- 20: breq 16 <-+ | (should not disassemble) + * 22: ret | (should not disassemble) + * | + * 30: ret <---+ (should not disassemble) + * + * Test restricted disassembly + * + */ + @Test + public void testDisassemblerRestricted() throws Exception { + + programBuilder.addBytesCall(10, 30); + programBuilder.addBytesBranchConditional(12, 20); + programBuilder.addBytesReturn(14); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesBranchConditional(20, 16); + programBuilder.addBytesReturn(22); + + programBuilder.addBytesReturn(30); + + AddressSet restrictSet = addrset(range(0, 19)); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), restrictSet); + assertEquals(addrset(range(10, 15)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(16), addr(20), addr(22), addr(30))); + + verifyNoBookmarks(); + + } + + /** + * + * 10: or + * 12: breq 14 --+ + * 14: or <----+ + * 16: or + * 18: ret (should not disassemble) + * + * Test restricted disassembly + * + */ + @Test + public void testDisassemblerRestricted2() throws Exception { + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesBranchConditional(12, 14); + programBuilder.addBytesFallthrough(14); + programBuilder.addBytesFallthrough(16); + programBuilder.addBytesReturn(18); + + AddressSet restrictSet = addrset(range(0, 17)); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), restrictSet); + assertEquals(addrset(range(10, 17)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(18))); + + verifyNoBookmarks(); + + } + + // + // TEST INSTRUCTION-SET LIMIT - FRAGMENTED DISASSEMBLY + // + + /** + * +-- 6: bral 12 (start) + * | + * | 10: callds 20 <-+ --+ + * +-> 12: _or | | + * 14: breq 10 ----+ | + * 16: ret | + * | + * 20: ret <-----------+ + * + * Test fragmented disassembly with InstructionSet size limit of 1 + * + */ + @Test + public void testDisassemblerLimit1() throws Exception { + + // Override instruction set limit + disassembler.setInstructionSetSizeLimit(1); + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesCallWithDelaySlot(10, 20); + programBuilder.addBytesBranchConditional(14, 10); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesReturn(20); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(10, 17), range(20, 21)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(10)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(12)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * +-- 6: bral 12 (start) + * | + * | 10: callds 20 <-+ --+ + * +-> 12: _or | | + * 14: breq 10 ----+ | + * 16: ret | + * | + * 20: ret <-----------+ + * + * Test fragmented disassembly with InstructionSet size limit of 2 + * + */ + @Test + public void testDisassemblerLimit2() throws Exception { + + // Override instruction set limit + disassembler.setInstructionSetSizeLimit(2); + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesCallWithDelaySlot(10, 20); + programBuilder.addBytesBranchConditional(14, 10); + programBuilder.addBytesReturn(16); + + programBuilder.addBytesReturn(20); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(10, 17), range(20, 21)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + Instruction instr = listing.getInstructionAt(addr(10)); + assertEquals(1, instr.getDelaySlotDepth()); + instr = listing.getInstructionAt(addr(12)); + assertTrue(instr.isInDelaySlot()); + + } + + /** + * +--10: breq 20 (start) + * | 12: call 30 --+ + * | 14: ret | + * | | + * +-> 20: or | + * +-- 22: bral 40 | + * | | + * | 30: or <----+ + * | 32: bral 40 -+ + * | | + * +->40: ret <---+ + * + * Test fragmented disassembly with InstructionSet size limit of 2 + * + */ + @Test + public void testDisassemblerLimit3() throws Exception { + + // Override instruction set limit + disassembler.setInstructionSetSizeLimit(2); + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesCall(12, 30); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesBranch(22, 40); + + programBuilder.addBytesFallthrough(30); + programBuilder.addBytesBranch(32, 40); + + programBuilder.addBytesReturn(40); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 23), range(30, 33), range(40, 41)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * 1000: or + * 1002: or + * ... + * 2000: or + * 2002: ret + * + * Test fragmented disassembly with InstructionSet size limit of 2 + * + */ + @Test + public void testDisassemblerLimit4() throws Exception { + + // Override instruction set limit + disassembler.setInstructionSetSizeLimit(2); + + for (long offset = 1000; offset <= 2000; offset += 2) { + programBuilder.addBytesFallthrough(offset); + } + programBuilder.addBytesReturn(2002); + + AddressSetView disAddrs = disassembler.disassemble(addr(1000), null); + assertEquals(addrset(range(1000, 2003)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + /** + * 10: nfctx #3 (start) -- writes no-flow context =3 @ 30 + * +-- 14: br 22 + * | + * | 20: nop <-+ + * +-> 22: breq 30 - | -+ + * 24: br 20 --+ | + * | + * 30: cop3 <-------+ + * 32: ret + * + * Limit Size: 4 + * InstructionSet Order: 10,14,22,24/20,(22),(24),(30)/30,32 + * () - indicates instructions blocked by those already added to program + * + * Test fragmented disassembly with InstructionSet size limit of 4 + * and flow priority given to code blocks already added to program. + * Flow priority is needed to ensure that block which consumes context + * is assured of being added. + * + */ + @Test + public void testDisassemblerFlowPriority() throws Exception { + + // Override instruction set limit + disassembler.setInstructionSetSizeLimit(4); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3, 30); + programBuilder.addBytesBranch(14, 22); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesBranchConditional(22, 30); + programBuilder.addBytesBranch(24, 20); + + programBuilder.addBytesCopInstruction(30); + programBuilder.addBytesReturn(32); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + +// openProgramInTool(); + + assertEquals(addrset(range(10, 15), range(20, 25), range(30, 33)), disAddrs); + + verifyContextRanges(30, 30, "nfctx", new ContextRangeValue(30, 30, 3)); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + + // + // TEST DUPLICATION WITH INSTRUCTIONS IN PROGRAM + // + + /** + * + * +-> 10: breq 20 --+ (start1) + * | 12: ret | + * | | 18: breq 30 -+ (start2) + * +-- 20: breq 10 <-+ ... | + * 22: ret | + * | + * 30: ret <---+ + * + * Test bumping into existing instructions in Program mid-block + * + */ + @Test + public void testDisassemblerMidBlockOverlapWithProgram() throws Exception { + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesReturn(12); + + programBuilder.addBytesBranchConditional(20, 10); + programBuilder.addBytesReturn(22); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 13), range(20, 23)), disAddrs); + + programBuilder.addBytesBranchConditional(18, 30); + + programBuilder.addBytesReturn(30); + + disAddrs = disassembler.disassemble(addr(18), null); + assertEquals(addrset(range(18, 19), range(30, 31)), disAddrs); + + verifyInstructionPresence(); + + verifyNoBookmarks(); + + } + +// public void testDisassemblerMidBlockOverlapWithProgramAndConflictPreservation() +// throws Exception { +// // TODO: having difficult time coming up with valid test case +// } + +// public void testDisassemblerMidBlockOverlapWithProgramAndConflictDetection() throws Exception { +// // TODO: having difficult time coming up with valid test case +// } + + // + // TEST BAD PARSE CASES + // + + /** + * 10: or + * 12: BAD (Unable to resolve constructor) + * 14: ret (Not parsed due to parse error @ 12) + * + * Test parse error + * + */ + @Test + public void testDisassemblerBadParse1() throws Exception { + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesBadInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(14))); + + verifyErrorBookmark(addr(12), "Unable to resolve constructor"); + + } + + /** + * 10: or + * 12: cop# (Unable to resolve constructor without context) + * 14: ret (Not parsed due to parse error @ 12) + * + * Test use of non-flow context in disassembly with expected parse error (not set) + * + */ + @Test + public void testDisassemblerBadParse2() throws Exception { + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(12), addr(14))); + + verifyErrorBookmark(addr(12), "Unable to resolve constructor"); + + } + + // + // TEST CONFLICT CASES + // + + /** + * 10: or + * 12: ret (Not parsed due to data conflict @ 13) + * + * Test data conflict error + * + */ + @Test + public void testDisassemblerDataConflict() throws Exception { + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesReturn(12); + + listing.createData(addr(13), ByteDataType.dataType); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(12))); + + verifyErrorBookmark(addr(12), "conflicting data"); + + } + + /** + * 10: or (start2) + * 12: nfctx 20,2 (4-byte instr not parsed due to instruction conflict @ 14) + * +->16: ret + * | + * | 14: imm (start1) (parsed tail of 4-byte nfctx instr) + * +--+ + * + * Test instruction conflict error + * + */ + @Test + public void testDisassemblerInstructionConflict() throws Exception { + + // instr at 14 is tail of nfctx but we need to add to program builder to register + // instruction location for verification purpose only + programBuilder.addBytesFallthrough(14); + + programBuilder.addBytesFallthrough(10); + programBuilder.addBytesFallthroughSetNoFlowContext(12, 2, 30);// overwrites instr bytes at 14 + programBuilder.addBytesReturn(16); + + AddressSetView disAddrs = disassembler.disassemble(addr(14), null); + assertEquals(addrset(range(14, 17)), disAddrs); + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(12))); + + verifyErrorBookmark(addr(12), "conflicting instruction"); + + } + + /** + * +--10: breq 20 + * | 12: breq 30 ---+ + * | 14: ret | + * | | + * +-> 20: or | + * 22: nfctx 18,2 | 24: imm (forced conflict) + * +-- 26: bral 40 | + * | | + * | 30: or <-----+ + * | 32: nfctx 28,2 + * | 36: bral 40 ---+ + * | | + * +->40: ret <-----+ (clear after first dis to allow flow from 36) + * + */ + @Test + public void testDisassemblerMultipathInstructionConflict1() throws Exception { + + // instr at 24 is tail of nfctx but we need to add to program builder to register + // instruction location for verification purpose only + programBuilder.addBytesFallthrough(24); + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesBranchConditional(12, 30); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesFallthroughSetNoFlowContext(22, 2, 18);// overwrites instr bytes at 24 + programBuilder.addBytesBranch(26, 40); + + programBuilder.addBytesFallthrough(30); + programBuilder.addBytesFallthroughSetNoFlowContext(32, 2, 18); + programBuilder.addBytesBranch(36, 40); + + programBuilder.addBytesReturn(40); + + AddressSetView disAddrs = disassembler.disassemble(addr(24), null); + assertEquals(addrset(range(24, 27), range(40, 41)), disAddrs); + + // clear instruction at 40 to allow flow from 36 + program.getListing().clearCodeUnits(addr(40), addr(40), true); + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 21), range(30, 37), range(40, 41)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(22))); + + verifyErrorBookmark(addr(22), "conflicting instruction"); + + } + + /** + * +--10: breq 20 + * | 12: breq 30 ---+ + * | 14: ret | + * | | + * +-> 20: or | + * 22: nfctx 18,2 | + * +-- 26: bral 40 | + * | | + * | 30: or <-----+ + * | 32: nfctx 28,2 34: imm (forced conflict) + * | 36: bral 40 ---+ + * | | + * +->40: ret <-----+ (clear after first dis to allow flow from 26) + * + */ + @Test + public void testDisassemblerMultipathInstructionConflict2() throws Exception { + + // instr at 24 is tail of nfctx but we need to add to program builder to register + // instruction location for verification purpose only + programBuilder.addBytesFallthrough(34); + + programBuilder.addBytesBranchConditional(10, 20); + programBuilder.addBytesBranchConditional(12, 30); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthrough(20); + programBuilder.addBytesFallthroughSetNoFlowContext(22, 2, 18); + programBuilder.addBytesBranch(26, 40); + + programBuilder.addBytesFallthrough(30); + programBuilder.addBytesFallthroughSetNoFlowContext(32, 2, 18);// overwrites instr bytes at 34 + programBuilder.addBytesBranch(36, 40); + + programBuilder.addBytesReturn(40); + + AddressSetView disAddrs = disassembler.disassemble(addr(34), null); + assertEquals(addrset(range(34, 37), range(40, 41)), disAddrs); + + // clear instruction at 40 to allow flow from 26 + program.getListing().clearCodeUnits(addr(40), addr(40), true); + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 27), range(30, 31), range(40, 41)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(32))); + + verifyErrorBookmark(addr(32), "conflicting instruction"); + + } + + /** + * 10: nfctx #3 (start) + * 12: cop # <-----+ (conflict due to varying context) + * 14: bral 20 -----+ | + * | | + * 20: nfctx 12,2 <-+ | (4-byte instr) + * 24: breq 12 ------+ + * 26: ret + * + * Test use of non-flow context in disassembly with expected parse error + * + */ + @Test + public void testDisassemblerInconsistentStartBlock() throws Exception { + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesBranch(14, 20); + + programBuilder.addBytesFallthroughSetNoFlowContext(20, 2, 12); + programBuilder.addBytesBranchConditional(24, 12); + programBuilder.addBytesReturn(26); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15), range(20, 27)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "inconsistent instruction prototype"); + + } + + /** + * 6: bral 12 --+ (start w/ nfctx=2 @ 12) + * | + * +-> 10: nfctx #3 | + * | 12: cop3 <--+ (conflict due to varying context) + * +-- 14: bral 10 + * + * Test use of non-flow context in disassembly with expected parse error + * + */ + @Test + public void testDisassemblerInconsistentMidBlock() throws Exception { + + program.getProgramContext().setValue(program.getRegister("nfctx"), addr(12), addr(12), + BigInteger.valueOf(2)); + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesBranch(14, 10); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(10, 15)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "inconsistent instruction prototype"); + + } + + /** + * 10: nfctx #3 (start1) + * 12: cop # <-----+ (conflict due to varying context) + * 14: ret | + * | + * 20: nfctx 12,2 | (start2) (4-byte instr) + * 24: breq 12 ------+ + * 26: ret + * + * Test use of non-flow context in disassembly with expected parse error + * + */ + @Test + public void testDisassemblerInconsistentStartBlockInProgram() throws Exception { + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + programBuilder.addBytesFallthroughSetNoFlowContext(20, 2, 12); + programBuilder.addBytesBranchConditional(24, 12); + programBuilder.addBytesReturn(26); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 15)), disAddrs); + + disAddrs = disassembler.disassemble(addr(20), null); + assertEquals(addrset(range(20, 27)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "inconsistent instruction prototype"); + + } + + /** + * 6: bral 12 --+ (start1 w/ nfctx=2 @ 12) + * | + * 10: nfctx #3 | (start2) + * 12: cop3 <--+ (conflict due to varying context) + * 14: ret + * + * Test use of non-flow context in disassembly with expected parse error + * + */ + @Test + public void testDisassemblerInconsistentMidBlockInProgram() throws Exception { + + program.getProgramContext().setValue(program.getRegister("nfctx"), addr(12), addr(12), + BigInteger.valueOf(2)); + + programBuilder.addBytesBranch(6, 12); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 3); + programBuilder.addBytesCopInstruction(12); + programBuilder.addBytesReturn(14); + + AddressSetView disAddrs = disassembler.disassemble(addr(6), null); + assertEquals(addrset(range(6, 7), range(12, 15)), disAddrs); + + disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "inconsistent instruction prototype"); + + } + +// public void testDisassemblerMultipleConflictsWithinBlock() throws Exception { +// // TODO: having difficult time coming up with valid test case +// } + + // + // TEST MEMORY RESTRICTIONS + // + + /** + * +-- 4: breq 11 (error bookmark on unaligned flow) + * | 6: ret (not disassembled - due to halted flow) + * | + * +-> 11: ret (not allowed - unaligned) + * + * Test unaligned disassembly + * + */ + @Test + public void testDisassemblerUnaligned() throws Exception { + + programBuilder.addBytesBranchConditional(4, 11); + programBuilder.addBytesReturn(6); + + programBuilder.addBytesReturn(11); + + AddressSetView disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 5)), disAddrs); + + verifyInstructionPresence(CollectionUtils.asSet(addr(6), addr(11))); + + verifyErrorBookmark(addr(4), "violates 2-byte instruction alignment"); + + } + + /** + * +-- 4: bral 10 + * | + * +-> 10: nfctx 20,2 (4-byte instr) + * 14: bral 20 --+ | (offcut conflict error @ 12) + * | | + * 20: cop #2 <-+ | + * 22: bral 12 ----+ (offcut conflict error) + * + * Test offcut-conflict disassembly + * + */ + @Test + public void testDisassemblerOffcutConflict() throws Exception { + + programBuilder.addBytesBranch(4, 10); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 2, 20); + programBuilder.addBytesBranch(14, 20); + + programBuilder.addBytesCopInstruction(20); + programBuilder.addBytesBranch(22, 12); + + AddressSetView disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 5), range(10, 15), range(20, 23)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "conflicting instruction"); + + } + + /** + * +-- 4: bral 10 + * | + * +-> 10: nfctx 20,2 (4-byte instr) + * 14: bral 20 --+ | (offcut conflict error @ 12) + * | | + * 20: cop #2 <-+ | + * 22: ret | + * | + * 30: bral 12 ----+ + * + * Test offcut-conflict disassembly + * + */ + @Test + public void testDisassemblerOffcutConflictInProgram() throws Exception { + + programBuilder.addBytesBranch(4, 10); + + programBuilder.addBytesFallthroughSetNoFlowContext(10, 2, 20); + programBuilder.addBytesBranch(14, 20); + + programBuilder.addBytesCopInstruction(20); + programBuilder.addBytesReturn(22); + + programBuilder.addBytesBranch(30, 12); + + AddressSetView disAddrs = disassembler.disassemble(addr(4), null); + assertEquals(addrset(range(4, 5), range(10, 15), range(20, 23)), disAddrs); + + disAddrs = disassembler.disassemble(addr(30), null); + assertEquals(addrset(range(30, 31)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(12), "conflicting instruction"); + + } + + /** + * 10: bral 200 (error on flow to non-existing memory) + * + * Test flow into non-existing memory + * + */ + @Test + public void testDisassemblerNoMemory() throws Exception { + + programBuilder.addBytesBranch(10, 0xffffffe0L); + + AddressSetView disAddrs = disassembler.disassemble(addr(10), null); + assertEquals(addrset(range(10, 11)), disAddrs); + + verifyInstructionPresence(); + + verifyErrorBookmark(addr(10), "non-existing memory"); + + } + +// public void testDisassemblerNonInitializedMemory() throws Exception { +// // TODO: currently we ignore attempts to disassemble into Non-Intialized Memory +// } + + @Test + public void testDisassemblerNoExecuteMemory() throws Exception { + // TODO: need to do this in master branch + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueContextTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueContextTest.java new file mode 100644 index 0000000000..21fa0b663e --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueContextTest.java @@ -0,0 +1,121 @@ +/* ### + * 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.program.model.lang; + +import static org.junit.Assert.assertEquals; + +import java.math.BigInteger; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; + +public class RegisterValueContextTest extends AbstractGhidraHeadlessIntegrationTest { + + private Language language; + + private Register regContext; + + @Before + public void setUp() throws Exception { + LanguageService languageService = getLanguageService(); + language = languageService.getLanguage(new LanguageID("x86:LE:32:default")); + + regContext = language.getContextBaseRegister();// 4-byte context reg + } + + @Test + public void testRegisterValueMask() { + + RegisterValue val = new RegisterValue(regContext, BigInteger.valueOf(0x12345678)); + BigInteger value = val.getUnsignedValue(); + assertEquals(0x12345678, value.longValue()); + BigInteger valueMask = val.getValueMask(); + assertEquals(0xffffffffL, valueMask.longValue()); + + RegisterValue newValue = new RegisterValue(regContext, value, valueMask); + assertEquals(0x12345678, newValue.getUnsignedValue().longValue()); + assertEquals(0xffffffffL, newValue.getValueMask().longValue()); + + } + + @Test + public void testBytes() { + + RegisterValue val = new RegisterValue(regContext, + new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 1, 2, 3, 4 }); + + assertEquals(0x01020304, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x01020304, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x01020304, val.getUnsignedValue().longValue()); + assertEquals(0x01020304, val.getSignedValue().longValue()); + assertEquals(0x0ffffffffL, val.getValueMask().longValue()); + + val = new RegisterValue(regContext, + new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xf0, (byte) 0xff, 1, 2, 3, 4 }); + + assertEquals(0x01020304, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x01020304, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x0fffff0ffL, val.getValueMask().longValue()); + } + + @Test + public void testBytesGrow() { + + RegisterValue val = + new RegisterValue(regContext, new byte[] { (byte) 0xff, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x12340000, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x12340000, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x0ffff0000L, val.getValueMask().longValue()); + + val = new RegisterValue(regContext, new byte[] { (byte) 0x10, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x10340000, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x10340000, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x10ff0000, val.getValueMask().longValue()); + } + + @Test + public void testBytesShrink() { + + RegisterValue val = new RegisterValue(regContext, new byte[] { (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0, 0, 0, 0 }); + + assertEquals(0x12345678, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x12345678, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x12345678, val.getUnsignedValue().longValue()); + assertEquals(0x12345678, val.getSignedValue().longValue()); + assertEquals(0x0ffffffffL, val.getValueMask().longValue()); + + val = new RegisterValue(regContext, new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xf0, + (byte) 0xff, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0, 0, 0, 0 }); + + assertEquals(0x12345078, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x12345078, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x0fffff0ffL, val.getValueMask().longValue()); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueTest.java new file mode 100644 index 0000000000..67443c1e1b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/model/lang/RegisterValueTest.java @@ -0,0 +1,162 @@ +/* ### + * 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.program.model.lang; + +import static org.junit.Assert.assertEquals; + +import java.math.BigInteger; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; + +public class RegisterValueTest extends AbstractGhidraHeadlessIntegrationTest { + + private Language language; + + private Register regEAX; + private Register regAX; + private Register regAH; + private Register regAL; + + private Register regEBX; + private Register regBX; + private Register regBH; + private Register regBL; + + private Register regCF; + + @Before + public void setUp() throws Exception { + LanguageService languageService = getLanguageService(); + language = languageService.getLanguage(new LanguageID("x86:LE:32:default")); + + regEAX = language.getRegister("EAX");// 4-byte reg within 8-byte parent + regAX = language.getRegister("AX"); + regAH = language.getRegister("AH"); + regAL = language.getRegister("AL"); + + regEBX = language.getRegister("EBX");// 4-byte reg within 8-byte parent + regBX = language.getRegister("BX"); + regBH = language.getRegister("BH"); + regBL = language.getRegister("BL"); + + regCF = language.getRegister("CF");// 1-byte reg (basereg) + } + + @Test + public void testRegisterValueMask() { + + RegisterValue valAH = new RegisterValue(regAH, BigInteger.valueOf(0x55)); + BigInteger value = valAH.getUnsignedValue(); + assertEquals(0x55, value.longValue()); + BigInteger valueMask = valAH.getValueMask(); + assertEquals(0xff, valueMask.longValue()); + + RegisterValue newValue = new RegisterValue(regBH, value, valueMask); + assertEquals(0x55, newValue.getUnsignedValue().longValue()); + assertEquals(0xff, newValue.getValueMask().longValue()); + + newValue = newValue.getRegisterValue(regEBX); + assertEquals(0x5500, newValue.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0xff00, newValue.getValueMask().longValue()); + + newValue = new RegisterValue(regBL, value, valueMask); + assertEquals(0x55, newValue.getUnsignedValue().longValue()); + assertEquals(0xff, newValue.getValueMask().longValue()); + + RegisterValue valEAX = valAH.getRegisterValue(regEAX).assign(regAL, newValue); + assertEquals(0x5555, valEAX.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0xffff, valEAX.getValueMask().longValue()); + + } + + @Test + public void testBytes() { + + RegisterValue val = new RegisterValue(regEBX, + new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, 0, 0, 0, 0, 0, 0, 0x12, 0x34 }); + + assertEquals(0x1234, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x1234, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x1234, val.getUnsignedValue().longValue()); + assertEquals(0x1234, val.getSignedValue().longValue()); + assertEquals(0x0ffffffffL, val.getValueMask().longValue()); + + val = new RegisterValue(regEBX, + new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xf0, (byte) 0xff, 0, 0, 0, 0, 0, 0, 0x12, 0x34 }); + + assertEquals(0x1034, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x1034, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x0fffff0ffL, val.getValueMask().longValue()); + } + + @Test + public void testBytesGrow() { + + RegisterValue val = + new RegisterValue(regEBX, new byte[] { (byte) 0xff, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x1234, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x1234, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x1234, val.getUnsignedValue().longValue()); + assertEquals(0x1234, val.getSignedValue().longValue()); + assertEquals(0x0ffffffffL, val.getValueMask().longValue()); + + val = new RegisterValue(regEBX, new byte[] { (byte) 0x10, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x1034, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x1034, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x10ff, val.getValueMask().longValue()); + } + + @Test + public void testBytesShrink() { + + RegisterValue val = + new RegisterValue(regCF, new byte[] { (byte) 0xff, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x34, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x34, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x34, val.getUnsignedValue().longValue()); + assertEquals(0x34, val.getSignedValue().longValue()); + assertEquals(0x0ffL, val.getValueMask().longValue()); + + val = new RegisterValue(regCF, new byte[] { (byte) 0xf0, (byte) 0xff, 0x12, 0x34 }); + + assertEquals(0x34, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x34, val.getSignedValueIgnoreMask().longValue()); + assertEquals(0x34, val.getUnsignedValue().longValue()); + assertEquals(0x34, val.getSignedValue().longValue()); + assertEquals(0x0ffL, val.getValueMask().longValue()); + + val = new RegisterValue(regCF, new byte[] { (byte) 0xff, (byte) 0xf0, 0x12, 0x34 }); + + assertEquals(0x30, val.getUnsignedValueIgnoreMask().longValue()); + assertEquals(0x30, val.getSignedValueIgnoreMask().longValue()); + assertEquals(null, val.getUnsignedValue()); + assertEquals(null, val.getSignedValue()); + assertEquals(0x0f0, val.getValueMask().longValue()); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryFileSystemTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryFileSystemTest.java new file mode 100644 index 0000000000..c4a938236f --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryFileSystemTest.java @@ -0,0 +1,428 @@ +/* ### + * 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.server; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.*; + +import db.*; +import db.buffers.BufferFile; +import db.buffers.ManagedBufferFile; +import ghidra.framework.client.ClientUtil; +import ghidra.framework.store.*; +import ghidra.framework.store.local.LocalFileSystem; +import ghidra.server.store.RepositoryFile; +import ghidra.server.store.RepositoryFolder; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.Msg; +import ghidra.util.exception.FileInUseException; +import utilities.util.FileUtilities; + +public class RepositoryFileSystemTest extends AbstractGhidraHeadedIntegrationTest { + + private static final String USER = ClientUtil.getUserName(); + + private File serverRoot; + + private RepositoryManager mgr; + private Repository repository; + + private List events = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + + File parent = createTempDirectory(getClass().getSimpleName()); + + // Create server instance + serverRoot = new File(parent, "My_Server"); + FileUtilities.deleteDir(serverRoot); + serverRoot.mkdir(); + + mgr = new RepositoryManager(serverRoot, false, false, 0, false); + mgr.getUserManager().addUser(USER); + + repository = mgr.createRepository(USER, "My_Repository"); + + LocalFileSystem fs = (LocalFileSystem) getInstanceField("fileSystem", repository); + fs.addFileSystemListener(new MyFileSystemListener()); + } + + @After + public void tearDown() throws Exception { + if (mgr != null) { + mgr.dispose(); + } + FileUtilities.deleteDir(serverRoot); + } + + private RepositoryFile createDatabase(String parentPath, String itemName, int maxVersion) + throws Exception { + + RepositoryFolder folder = repository.getFolder(USER, parentPath, true); + + DBHandle dbh = new DBHandle(); + long id = dbh.startTransaction(); + Schema schema = + new Schema(0, "key", new Class[] { IntField.class }, new String[] { "dummy" }); + dbh.createTable("test", schema); + dbh.endTransaction(id, true); + ManagedBufferFile bf = folder.createDatabase(itemName, FileIDFactory.createFileID(), + dbh.getBufferSize(), "Database", USER, null); + bf.setVersionComment("Version 1"); + dbh.saveAs(bf, true, null); + long checkoutId = bf.getCheckinID(); + assertNotNull(dbh.getTable("test")); + dbh.close(); + + RepositoryFile file = folder.getFile(itemName); + assertNotNull(file); + assertEquals(1, file.getItem().getVersion()); + + for (int i = 2; i <= maxVersion; i++) { + + // Checkout and open for update + bf = file.openDatabase(checkoutId, USER); + dbh = new DBHandle(bf); + assertTrue(dbh.canUpdate()); + Table testTable = dbh.getTable("test"); + assertEquals(i - 2, testTable.getRecordCount()); + + // Verify that update is not permitted for second open + BufferFile bf2 = file.openDatabase(1, -1, USER); + DBHandle dbh2 = new DBHandle(bf2); + assertTrue(!dbh2.canUpdate()); + assertNotNull(dbh2.getTable("test")); + dbh2.close(); + + // Add record + long txId = dbh.startTransaction(); + Record rec = schema.createRecord(i); + rec.setIntValue(0, i); + testTable.putRecord(rec); + Msg.debug(this, "Added record to test table, key=" + i); + Msg.debug(this, + "Added record to Record count for test table: " + testTable.getRecordCount()); + dbh.endTransaction(txId, true); + + // Create new version + Msg.debug(this, "Saving database version " + i); + dbh.save("Version " + i, new MyChangeSet(), null); + dbh.close(); + + // Verify item current version + assertEquals(i, file.getItem().getVersion()); + + // Check version history + Version[] versions = file.getVersions(USER); + assertEquals(i, versions.length); + for (int n = 0; n < i; n++) { + assertEquals(n + 1, versions[n].getVersion()); + assertEquals("Version " + (n + 1), versions[n].getComment()); + assertEquals(USER, versions[n].getUser()); + } + + } + file.terminateCheckout(checkoutId, USER, false); + return file; + } + + @Test + public void testDeleteDataBaseVersions() { + try { + RepositoryFolder rootFolder = repository.getFolder(USER, "/", true); + RepositoryFolder[] folders = rootFolder.getFolders(); + assertNotNull(folders); + assertEquals(0, folders.length); + + RepositoryFile file = createDatabase("/abc", "fred", 3); + + RepositoryFolder folder = repository.getFolder(USER, "/abc", false); + assertNotNull(folder); + + // Can't delete open version + // BufferFile bf = item.open((int)1); + // try { + // item.delete(1); + // fail(); + // } catch (FileInUseException e) { + // // expected + // } + // finally { + // bf.close(); + // } + + // Can't delete checked-out version + ItemCheckoutStatus coStatus = file.checkout(CheckoutType.NORMAL, USER, null); + try { + file.delete(3, USER); + Assert.fail(); + } + catch (FileInUseException e) { + // expected + } + file = folder.getFile("fred"); + assertNotNull(file); + + // delete oldest version + file.delete(1, USER); + Thread.sleep(50); + file = folder.getFile("fred"); + assertNotNull(file); + + // verify that version 2 and 3 are still available + assertEquals(3, file.getItem().getVersion()); + Version[] versions = file.getVersions(USER); + assertEquals(2, versions.length); + assertEquals(2, versions[0].getVersion()); + assertEquals(3, versions[1].getVersion()); + + file.terminateCheckout(coStatus.getCheckoutId(), USER, false); + Thread.sleep(50); + + // delete current version + file.delete(3, USER); + Thread.sleep(50); + file = folder.getFile("fred"); + assertNotNull(file); + + // verify that version 2 is still available + assertEquals(2, file.getItem().getVersion()); + versions = file.getVersions(USER); + assertEquals(1, versions.length); + assertEquals(2, versions[0].getVersion()); + + // Open version 2 for check + DBHandle dbh = new DBHandle(file.openDatabase(2, -1, USER)); + try { + Table testTable = dbh.getTable("test"); + assertEquals(1, testTable.getRecordCount()); + Record rec = testTable.getRecord(2); + assertNotNull(rec); + assertEquals(2, rec.getIntValue(0)); + } + finally { + dbh.close(); + } + + // delete last version + file.delete(2, USER); + Thread.sleep(50); + file = folder.getFile("fred"); + assertNull(file); + + folders = rootFolder.getFolders(); + assertNotNull(folders); + assertEquals(0, folders.length); + + Thread.sleep(500);// wait for events + + assertEquals(8, events.size()); + checkEvent("Folder Created", "/", "abc", null, null, events.get(0)); + checkEvent("Item Created", "/abc", "fred", null, null, events.get(1)); + checkEvent("Item Changed", "/abc", "fred", null, null, events.get(2));// version 2 created + checkEvent("Item Changed", "/abc", "fred", null, null, events.get(3));// version 3 created + checkEvent("Item Changed", "/abc", "fred", null, null, events.get(4));// version 1 deleted + checkEvent("Item Changed", "/abc", "fred", null, null, events.get(5));// version 3 deleted + checkEvent("Item Deleted", "/abc", "fred", null, null, events.get(6));// last version deleted + checkEvent("Folder Deleted", "/", "abc", null, null, events.get(7)); + } + catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.toString()); + } + } + + private void checkEvent(String op, String path, String name, String newPath, String newName, + Object evObj) { + MyEvent event = (MyEvent) evObj; + MyEvent ev = new MyEvent(op, path, name, newPath, newName); + assertEquals(ev, event); + } + + class MyChangeSet implements DBChangeSet { + + /* + * @see ghidra.framework.model.ChangeSet#read(ghidra.framework.store.db.DBHandle) + */ + @Override + public void read(DBHandle dbh) throws IOException { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#write(ghidra.framework.store.db.DBHandle) + */ + @Override + public void write(DBHandle dbh, boolean isRecoverSave) throws IOException { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#clear() + */ + public void clear() { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#undo() + */ + public void undo() { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#redo() + */ + public void redo() { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#setMaxUndos(int) + */ + public void setMaxUndos(int maxUndos) { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#clearUndo() + */ + public void clearUndo() { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#startTransaction() + */ + public void startTransaction() { + // TODO Auto-generated method stub + + } + + /* + * @see ghidra.framework.model.ChangeSet#endTransaction(boolean) + */ + public void endTransaction(boolean commit) { + // TODO Auto-generated method stub + + } + + } + + class MyFileSystemListener implements FileSystemListener { + @Override + public void folderCreated(String parentPath, String name) { + events.add(new MyEvent("Folder Created", parentPath, name, null, null)); + } + + @Override + public void itemCreated(String parentPath, String name) { + events.add(new MyEvent("Item Created", parentPath, name, null, null)); + } + + @Override + public void folderDeleted(String parentPath, String name) { + events.add(new MyEvent("Folder Deleted", parentPath, name, null, null)); + } + + @Override + public void folderMoved(String parentPath, String name, String newParentPath) { + events.add(new MyEvent("Folder Moved", parentPath, name, newParentPath, null)); + } + + @Override + public void folderRenamed(String parentPath, String oldFolderName, String newFolderName) { + events.add( + new MyEvent("Folder Renamed", parentPath, oldFolderName, null, newFolderName)); + } + + @Override + public void itemDeleted(String folderPath, String itemName) { + events.add(new MyEvent("Item Deleted", folderPath, itemName, null, null)); + } + + @Override + public void itemRenamed(String folderPath, String oldItemName, String newItemName) { + events.add(new MyEvent("Item Renamed", folderPath, oldItemName, null, newItemName)); + } + + @Override + public void itemMoved(String parentPath, String name, String newParentPath, + String newName) { + events.add(new MyEvent("Item Moved", parentPath, name, newParentPath, newName)); + } + + @Override + public void itemChanged(String parentPath, String itemName) { + events.add(new MyEvent("Item Changed", parentPath, itemName, null, null)); + } + + @Override + public void syncronize() { + // Nothing to do + } + } +} + +class MyEvent { + String op; + String parentPath; + String name; + String newParentPath; + String newName; + + MyEvent(String op, String parentPath, String name, String newParentPath, String newName) { + this.op = op; + this.parentPath = parentPath; + this.name = name; + this.newParentPath = newParentPath; + this.newName = newName; + } + + @Override + public boolean equals(Object obj) { + MyEvent other = (MyEvent) obj; + return eq(op, other.op) && eq(parentPath, other.parentPath) && eq(name, other.name) && + eq(newParentPath, other.newParentPath) && eq(newName, other.newName); + } + + private boolean eq(String s1, String s2) { + if (s1 == null) { + return s2 == null; + } + return s1.equals(s2); + } + + @Override + public String toString() { + return op + " " + parentPath + " " + name + " " + newParentPath + " " + newName; + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryManagerTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryManagerTest.java new file mode 100644 index 0000000000..273ed4ae52 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryManagerTest.java @@ -0,0 +1,213 @@ +/* ### + * 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.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.File; + +import org.junit.*; + +import ghidra.framework.remote.User; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.exception.DuplicateFileException; +import ghidra.util.exception.UserAccessException; + +public class RepositoryManagerTest extends AbstractGhidraHeadedIntegrationTest { + + private RepositoryManager mgr; + private File root; + + public RepositoryManagerTest() { + super(); + } + + @Before + public void setUp() throws Exception { + File parent = createTempDirectory(getName()); + root = new File(parent, "Repositories"); + if (root.exists()) { + deleteFiles(root); + root.delete(); + } + root.mkdir(); + writeUserList(root); + } + + @After + public void tearDown() throws Exception { + if (mgr != null) { + mgr.dispose(); + } + deleteFiles(root); + root.delete(); + } + + @Test + public void testCreateRepositoryManager() throws Exception { + + mgr = new RepositoryManager(root, false, false, 0, false); + assertNotNull(mgr); + + String[] userNames = mgr.getAllUsers("User_0"); + assertEquals(10, userNames.length); + } + + @Test + public void testCreateRepositoryManagerWithAnonymous() throws Exception { + + mgr = new RepositoryManager(root, false, false, 0, true); + assertNotNull(mgr); + + String[] userNames = mgr.getAllUsers("User_0"); + assertEquals(10, userNames.length); + + userNames = mgr.getAllUsers(UserManager.ANONYMOUS_USERNAME); + assertEquals(0, userNames.length); + } + + @Test + public void testCreateRepository() throws Exception { + mgr = new RepositoryManager(root, false, false, 0, false); + + Repository rep = mgr.createRepository("User_0", "REPOSITORY_A"); + assertNotNull(rep); + } + + @Test + public void testCreateRepositoryAnonymous() throws Exception { + mgr = new RepositoryManager(root, false, false, 0, true); + + Repository rep = mgr.createRepository("User_0", "REPOSITORY_A"); + assertNotNull(rep); + + try { + mgr.createRepository(UserManager.ANONYMOUS_USERNAME, "REPOSITORY_B"); + Assert.fail("Expected UserAccessException"); + } + catch (UserAccessException e) { + // expected + } + } + + @Test + public void testCreateDuplicateRepository() throws Exception { + mgr = new RepositoryManager(root, false, false, 0, false); + mgr.createRepository("User_0", "REPOSITORY_A"); + try { + mgr.createRepository("User_5", "REPOSITORY_A"); + Assert.fail("Should have gotten DuplicateNameException!"); + } + catch (DuplicateFileException e) { + } + } + + @Test + public void testGetRepository() throws Exception { + mgr = new RepositoryManager(root, false, false, 0, true); + Repository rep1 = mgr.createRepository("User_0", "REPOSITORY_A"); + addUsers("User_0", true, rep1); + + Repository rep2 = mgr.createRepository("User_0", "REPOSITORY_B"); + addUsers("User_0", false, rep2); + + Repository rep3 = mgr.createRepository("User_9", "REPOSITORY_9A"); + addUsers("User_9", false, rep3); + + Repository rep4 = mgr.createRepository("User_9", "REPOSITORY_9B"); + addUsers("User_9", false, rep4); + + assertEquals(rep1, mgr.getRepository("User_1", "REPOSITORY_A")); + assertEquals(rep1, mgr.getRepository(UserManager.ANONYMOUS_USERNAME, "REPOSITORY_A")); + assertEquals(rep2, mgr.getRepository("User_2", "REPOSITORY_B")); + try { + mgr.getRepository(UserManager.ANONYMOUS_USERNAME, "REPOSITORY_B"); + Assert.fail("Expected UserAccessException"); + } + catch (UserAccessException e) { + // expected + } + assertEquals(rep3, mgr.getRepository("User_3", "REPOSITORY_9A")); + assertEquals(rep4, mgr.getRepository("User_4", "REPOSITORY_9B")); + } + + @Test + public void testGetRepositoryBadUser() throws Exception { + mgr = new RepositoryManager(root, false, false, 0, false); + mgr.createRepository("User_0", "REPOSITORY_A"); + + try { + mgr.getRepository("unknownUser", "REPOSITORY_A"); + Assert.fail("Should not have been able to get repository!"); + } + catch (UserAccessException e) { + } + + try { + mgr.getRepository(UserManager.ANONYMOUS_USERNAME, "REPOSITORY_A"); + Assert.fail("Should not have been able to get repository!"); + } + catch (UserAccessException e) { + } + } + + private void addUsers(String currentUser, boolean allowAnonymousAccess, Repository rep) + throws Exception { + User[] users = new User[10]; + for (int i = 0; i < 10; i++) { + String name = "User_" + i; + int type = User.READ_ONLY; + if (name.equals(currentUser)) { + type = User.ADMIN; + } + users[i] = new User("User_" + i, type); + } + rep.setUserList(currentUser, users, allowAnonymousAccess); + } + + private void writeUserList(File repositoryRoot) throws Exception { + + String[] userNames = new String[10]; + for (int i = 0; i < userNames.length; i++) { + userNames[i] = "User_" + i; + } + + ServerTestUtil.createUsers(repositoryRoot.getAbsolutePath(), userNames); + } + + /** + * Recursive method to delete files in the given parent directory. + */ + private boolean deleteFiles(File parent) { + + File[] f = parent.listFiles(); + for (File element : f) { + if (element.isDirectory()) { + if (!deleteFiles(element)) { + return false; + } + element.delete(); + } + else if (!element.delete()) { + return false; + } + } + return true; + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryTest.java new file mode 100644 index 0000000000..67970b3a47 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/server/RepositoryTest.java @@ -0,0 +1,144 @@ +/* ### + * 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.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.File; + +import org.junit.*; + +import ghidra.framework.remote.User; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.NamingUtilities; +import ghidra.util.SystemUtilities; +import ghidra.util.exception.UserAccessException; +import utilities.util.FileUtilities; + +public class RepositoryTest extends AbstractGhidraHeadedIntegrationTest { + + private static final String REPOSITORY_NAME = "My_Repository"; + + private File serverRoot; + private RepositoryManager mgr; + private Repository repository; + private String userName; + + @Before + public void setUp() throws Exception { + + userName = SystemUtilities.getUserName(); + + File parent = createTempDirectory(getClass().getSimpleName()); + + // Create repository manager with no users + serverRoot = new File(parent, "My_Server"); + FileUtilities.deleteDir(serverRoot); + serverRoot.mkdir(); + + mgr = new RepositoryManager(serverRoot, false, false, 0, false); + mgr.getUserManager().addUser(userName); + + repository = mgr.createRepository(userName, REPOSITORY_NAME); + } + + @Test + public void testGetRepositoryName() throws Exception { + assertEquals(REPOSITORY_NAME, repository.getName()); + } + + @Test + public void testSetGetUserList() throws Exception { + User[] users = new User[5]; + users[0] = new User("user-a", User.READ_ONLY); + users[1] = new User("user-b", User.WRITE); + users[2] = new User("user-c", User.ADMIN); + users[3] = new User("user-d", User.READ_ONLY); + users[4] = new User(userName, User.ADMIN); + + repository.setUserList(userName, users, false); + + User[] reportedUsers = repository.getUserList(userName); + assertEquals(users.length, reportedUsers.length); + + for (int i = 0; i < users.length; i++) { + assertEquals(users[i].getName(), reportedUsers[i].getName()); + assertEquals(users[i].getPermissionType(), reportedUsers[i].getPermissionType()); + } + } + + @Test + public void testSetListBadUser() throws Exception { + + User[] users = new User[5]; + users[0] = new User("user-a", User.READ_ONLY); + users[1] = new User("user-b", User.WRITE); + users[2] = new User("user-c", User.ADMIN); + users[3] = new User("user-d", User.READ_ONLY); + users[4] = new User(userName, User.WRITE); + try { + repository.setUserList(userName, users, false); + Assert.fail("Should not have been able to change current user's access!"); + } + catch (UserAccessException e) { + } + + users[3] = new User("user-x", User.ADMIN); + try { + repository.setUserList(userName, users, false); + Assert.fail("Should not have been able to set the user list!"); + } + catch (UserAccessException e) { + } + + users[4] = new User(userName, User.ADMIN); + repository.setUserList(userName, users, false); + + User[] reportedUsers = repository.getUserList(userName); + assertEquals(users.length, reportedUsers.length); + for (int i = 0; i < users.length; i++) { + assertEquals(users[i].getName(), reportedUsers[i].getName()); + assertEquals(users[i].getPermissionType(), reportedUsers[i].getPermissionType()); + } + } + + @Test + public void testExistingRepository() throws Exception { + + User[] users = new User[5]; + users[0] = new User("user-a", User.READ_ONLY); + users[1] = new User("user-b", User.WRITE); + users[2] = new User("user-c", User.ADMIN); + users[3] = new User("user-d", User.READ_ONLY); + users[4] = new User(userName, User.ADMIN); + + repository.setUserList(userName, users, false); + + File repRoot = new File(serverRoot, NamingUtilities.mangle(REPOSITORY_NAME)); + Repository rep = new Repository(mgr, null, repRoot, REPOSITORY_NAME); + assertNotNull(rep); + + User[] reportedUsers = rep.getUserList(userName); + assertEquals(users.length, reportedUsers.length); + for (int i = 0; i < users.length; i++) { + assertEquals(users[i].getName(), reportedUsers[i].getName()); + assertEquals(users[i].getPermissionType(), reportedUsers[i].getPermissionType()); + } + + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/util/task/CachingSwingWorkerTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/util/task/CachingSwingWorkerTest.java new file mode 100644 index 0000000000..1ed85f0561 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/util/task/CachingSwingWorkerTest.java @@ -0,0 +1,292 @@ +/* ### + * 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.util.task; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.SwingWorker; + +import org.junit.Test; + +import docking.test.AbstractDockingTest; +import sun.awt.AppContext; +import sun.swing.AccumulativeRunnable; + +public class CachingSwingWorkerTest extends AbstractDockingTest { + + private static final int ITEM_COUNT = 10; + + @Test + public void testSimpleThreadCase() throws InterruptedException { + TestWorker worker = new TestWorker(); + + MyThread r1 = new MyThread(0, worker); + MyThread r2 = new MyThread(0, worker); + MyThread r3 = new MyThread(10, worker); + MyThread r4 = new MyThread(20, worker); + + worker.setDone(); + r1.join(); + r2.join(); + r3.join(); + r4.join(); + + assertEquals("number of runs", 1, worker.getNumRuns()); + + assertEquals(ITEM_COUNT, r1.size()); + assertEquals(ITEM_COUNT, r2.size()); + assertEquals(ITEM_COUNT, r3.size()); + assertEquals(ITEM_COUNT, r4.size()); + + } + + @Test + public void testSimpleSwingThreadCase() { + + disasbleTimerUsage(); + + TestWorker worker = new TestWorker(); + worker.setTaskDialogDelay(0); + + ClientRunnable runnable = new ClientRunnable(worker); + runSwing(runnable, false); + + TaskDialog dialog = waitForDialogComponent(null, TaskDialog.class, 2000); + assertNotNull(dialog); + + worker.setDone(); + worker.get(TaskMonitorAdapter.DUMMY_MONITOR); + + waitForPostedSwingRunnables(); + assertTrue(!dialog.isVisible()); + assertEquals("number of runs", 1, worker.getNumRuns()); + + assertEquals(ITEM_COUNT, runnable.size()); + } + + @Test + public void testSwingAndAnotherThreadCase() throws InterruptedException { + + disasbleTimerUsage(); + + TestWorker worker = new TestWorker(); + worker.setTaskDialogDelay(0); + + ClientRunnable runnable = new ClientRunnable(worker); + runSwing(runnable, false); + + MyThread r1 = new MyThread(100, worker); + + TaskDialog dialog = waitForDialogComponent(null, TaskDialog.class, 2000); + assertNotNull(dialog); + + worker.setDone(); + worker.get(TaskMonitorAdapter.DUMMY_MONITOR); + r1.join(); + assertEquals(ITEM_COUNT, r1.size()); + + waitForPostedSwingRunnables(); + assertTrue(!dialog.isVisible()); + assertEquals("number of runs", 1, worker.getNumRuns()); + + assertEquals(ITEM_COUNT, runnable.size()); + } + + @Test + public void testSwingAfterAnotherThreadCase() throws InterruptedException { + + disasbleTimerUsage(); + + TestWorker worker = new TestWorker(); + worker.setTaskDialogDelay(0); + + MyThread r1 = new MyThread(0, worker); + Thread.sleep(50); + ClientRunnable runnable = new ClientRunnable(worker); + runSwing(runnable, false); + + TaskDialog dialog = waitForDialogComponent(null, TaskDialog.class, 2000); + assertNotNull(dialog); + + worker.setDone(); + worker.get(TaskMonitorAdapter.DUMMY_MONITOR); + r1.join(); + assertEquals(ITEM_COUNT, r1.size()); + + waitForPostedSwingRunnables(); + assertTrue(!dialog.isVisible()); + assertEquals("number of runs", 1, worker.getNumRuns()); + + assertEquals(ITEM_COUNT, runnable.size()); + } + + @Test + public void testCancelled() { + TestWorker worker = new TestWorker(); + worker.setTaskDialogDelay(0); + + ClientRunnable runnable = new ClientRunnable(worker); + runSwing(runnable, false); + runSwing(runnable, false); + + TaskDialog dialog = waitForDialogComponent(null, TaskDialog.class, 2000); + assertTrue(dialog.getTitle().contains("Test Worker")); + assertNotNull(dialog); + + dialog.cancel(); + worker.get(TaskMonitorAdapter.DUMMY_MONITOR); + + waitForPostedSwingRunnables(); + assertTrue(!dialog.isVisible()); + assertEquals("number of runs", 1, worker.getNumRuns()); + + assertTrue(worker.wasCancelled); + + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void disasbleTimerUsage() { + AccumulativeRunnable nonTimerAccumulativeRunnable = + new AccumulativeRunnable() { + @Override + protected void run(List args) { + for (Runnable runnable : args) { + runnable.run(); + } + } + }; + + Object key = getInstanceField("DO_SUBMIT_KEY", SwingWorker.class); + + AppContext appContext = AppContext.getAppContext(); + appContext.put(key, nonTimerAccumulativeRunnable); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + class MyThread extends Thread { + ClientRunnable runnable; + + MyThread(int delay, TestWorker worker) { + runnable = new ClientRunnable(worker); + try { + Thread.sleep(delay); + } + catch (InterruptedException e) { + // whatever + } + start(); + } + + @Override + public void run() { + runnable.run(); + } + + int size() { + if (runnable == null) { + return 0; + } + return runnable.size(); + } + } + + class TestWorker extends CachingSwingWorker> { + private volatile boolean done = false; + private int numRuns = 0; + private volatile boolean wasCancelled = false; + + public TestWorker() { + super("Test Worker", true); + } + + @Override + protected List runInBackground(TaskMonitor monitor) { + monitor.initialize(ITEM_COUNT); + numRuns++; + List list = new ArrayList(); + int count = 0; + while (!done || count < ITEM_COUNT) { + if (monitor.isCancelled()) { + wasCancelled = true; + break; + } + + if (count < ITEM_COUNT) { + String message = "line " + count; + list.add(message); + monitor.setProgress(count); + monitor.setMessage(message); + } + + count++; + + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + if (count > 600) { + list.add("timeout"); + break; + } + } + + return list; + } + + void setDone() { + done = true; + } + + boolean wasCancelled() { + return wasCancelled; + } + + int getNumRuns() { + return numRuns; + } + } + + class ClientRunnable implements Runnable { + List result; + private TestWorker worker; + + ClientRunnable(TestWorker worker) { + this.worker = worker; + } + + public int size() { + return result == null ? 0 : result.size(); + } + + @Override + public void run() { + result = worker.get(TaskMonitorAdapter.DUMMY_MONITOR); + } + } +}