diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java index a7d381c490..586760eff6 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java @@ -1025,8 +1025,7 @@ public class BulkSignatures implements AutoCloseable { } @Override - protected void process(Program program, TaskMonitor monitor) - throws IOException, LSHException { + protected void process(Program program, TaskMonitor monitor) throws IOException { // NOTE: task monitor not used by DescriptionManager String md5string = program.getExecutableMD5(); if ((md5string == null) || (md5string.length() < 10)) { @@ -1059,9 +1058,8 @@ public class BulkSignatures implements AutoCloseable { manager.saveXml(fwrite); } } - catch (LSHException | IOException e) { - Msg.error(this, e.getMessage()); - throw e; + catch (LSHException e) { + throw new IOException("Program signature generation failure: " + e.getMessage()); } } } @@ -1084,8 +1082,7 @@ public class BulkSignatures implements AutoCloseable { } @Override - protected void process(Program program, TaskMonitor monitor) - throws IOException, LSHException, DecompileException { + protected void process(Program program, TaskMonitor monitor) throws IOException { // NOTE: task monitor not used by DescriptionManager String md5string = program.getExecutableMD5(); if ((md5string == null) || (md5string.length() < 10)) { @@ -1121,9 +1118,8 @@ public class BulkSignatures implements AutoCloseable { manager.saveXml(fwrite); fwrite.close(); } - catch (DecompileException | LSHException | IOException e) { - Msg.error(this, e.getMessage()); - throw e; + catch (DecompileException | LSHException e) { + throw new IOException("Program signature generation failure: " + e.getMessage()); } } } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java index 80152eb3a1..8163027b42 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java @@ -15,13 +15,10 @@ */ package ghidra.features.bsim.query.ingest; -import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import ghidra.features.bsim.query.LSHException; -import ghidra.framework.client.NotConnectedException; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.framework.protocol.ghidra.*; @@ -38,11 +35,11 @@ public abstract class IterateRepository { * Perform processing on program obtained from repository. * @param program program obtained from repository * @param monitor processing task monitor - * @throws Exception if an error occured during processing. + * @throws IOException if an error occured during processing. * @throws CancelledException if processing was cancelled */ protected abstract void process(Program program, TaskMonitor monitor) - throws Exception, CancelledException; + throws IOException, CancelledException; /** * Process the specified repository URL @@ -59,93 +56,32 @@ public abstract class IterateRepository { throw new MalformedURLException("Unsupported repository URL: " + ghidraURL); } - URL repoURL = GhidraURL.getProjectURL(ghidraURL); - String path = GhidraURL.getProjectPathname(ghidraURL); + GhidraURLQuery.queryUrl(ghidraURL, new GhidraURLResultHandlerAdapter(true) { - String finalelement = null; - path = path.trim(); - if (!path.endsWith("/")) { - int pos = path.lastIndexOf('/'); - if (pos >= 0) { - String tmp = path.substring(0, pos + 1); - if (tmp.length() != 0 && !tmp.equals("/")) { - finalelement = path.substring(pos + 1); // A possible file name at the end of the path - path = tmp; + @Override + public void processResult(DomainFolder domainFolder, URL url, TaskMonitor m) + throws IOException, CancelledException { - if (GhidraURL.isServerRepositoryURL(ghidraURL)) { - ghidraURL = new URL(repoURL + path); - } - else { - ghidraURL = new URL(repoURL + "?" + path); - } - } - } - } - - try { - GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection(); - - Msg.debug(IterateRepository.class, "Opening ghidra repository: " + ghidraURL); - Object obj = c.getContent(); - if (!(obj instanceof GhidraURLWrappedContent)) { - throw new IOException("Connect to repository folder failed"); - } - - Object consumer = new Object(); - - GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj; - Object content = null; - try { - content = wrappedContent.getContent(consumer); - if (!(content instanceof DomainFolder)) { - throw new IOException("Connect to repository folder failed"); - } - - DomainFolder folder = (DomainFolder) content; - - int totalFiles = getTotalFileCount(folder); + int totalFiles = getTotalFileCount(domainFolder); monitor.setMaximum(totalFiles); monitor.setShowProgressValue(true); - if (finalelement != null) { - DomainFolder subfolder = folder.getFolder(finalelement); - - if (subfolder != null) { - folder = subfolder; - // fall thru to the DomainFile and DomainFolder loop - } - else { - DomainFile file = folder.getFile(finalelement); - - if (file == null) { - throw new IOException("Bad folder/file element: " + finalelement); - } - - process(file, monitor); - return; - } - } - - process(folder, monitor); + process(domainFolder, monitor); } - finally { - if (content != null) { - wrappedContent.release(content, consumer); - } + + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor m) + throws IOException, CancelledException { + process(domainFile, monitor); } - } - catch (NotConnectedException e) { - throw new IOException( - "Ghidra repository connection failed (" + repoURL + "): " + e.getMessage()); - } - catch (FileNotFoundException e) { - throw new IOException("Repository path not found: " + path); - } + + }, monitor); + } private void process(DomainFolder folder, TaskMonitor monitor) - throws Exception, CancelledException { + throws IOException, CancelledException { for (DomainFile file : folder.getFiles()) { monitor.checkCancelled(); @@ -177,7 +113,7 @@ public abstract class IterateRepository { } private void process(DomainFile file, TaskMonitor monitor) - throws Exception, CancelledException { + throws IOException, CancelledException { // Do not follow folder-links or consider program links. Using content type // to filter is best way to control this. If program links should be considered @@ -189,12 +125,11 @@ public abstract class IterateRepository { } Program program = null; - Object consumer = new Object(); try { Msg.debug(IterateRepository.class, "Processing " + file.getPathname() + "..."); monitor.setMessage("Processing: " + file.getName()); monitor.incrementProgress(1); - program = (Program) file.getReadOnlyDomainObject(consumer, -1, monitor); + program = (Program) file.getReadOnlyDomainObject(this, -1, monitor); process(program, monitor); } catch (VersionException e) { @@ -203,7 +138,7 @@ public abstract class IterateRepository { } finally { if (program != null) { - program.release(consumer); + program.release(this); } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index 66163fc318..c08498684f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -48,7 +48,6 @@ import ghidra.framework.remote.User; import ghidra.framework.store.LockException; import ghidra.framework.store.local.LocalFileSystem; import ghidra.program.database.ProgramContentHandler; -import ghidra.program.database.ProgramDB; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.Program; import ghidra.program.util.GhidraProgramUtilities; @@ -310,52 +309,55 @@ public class HeadlessAnalyzer { Msg.info(HeadlessAnalyzer.class, "HEADLESS: execution starts"); - GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection(); - c.setReadOnly(options.readOnly); // writable repository connection + // force explicit folder access since file may have same name as folder + ghidraURL = GhidraURL.getFolderURL(ghidraURL); - if (c.getRepositoryName() == null) { - throw new MalformedURLException("Unsupported repository URL: " + ghidraURL); - } + Msg.info(this, "Opening ghidra repository folder: " + ghidraURL); - Msg.info(this, "Opening ghidra repository project: " + ghidraURL); - Object obj = c.getContent(); - if (!(obj instanceof GhidraURLWrappedContent)) { - throw new IOException( - "Connect to repository folder failed. Response code: " + c.getStatusCode()); - } - GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj; - Object content = null; - try { - content = wrappedContent.getContent(this); - if (!(content instanceof DomainFolder)) { - throw new IOException("Connect to repository folder failed"); - } + GhidraURLQuery.queryRepositoryUrl(ghidraURL, options.readOnly, + new GhidraURLResultHandlerAdapter() { - DomainFolder folder = (DomainFolder) content; - project = new HeadlessProject(getProjectManager(), c); + @Override + public void processResult(DomainFolder domainFolder, URL url, + TaskMonitor monitor) throws IOException, CancelledException { + try { + project = new HeadlessProject(getProjectManager(), + domainFolder.getProjectData()); - if (!checkUpdateOptions()) { - return; // TODO: Should an exception be thrown? - } + if (!checkUpdateOptions()) { + return; // TODO: Should an exception be thrown? + } - if (options.runScriptsNoImport) { - processNoImport(folder.getPathname()); - } - else { - processWithImport(folder.getPathname(), filesToImport); - } - } - catch (FileNotFoundException e) { - throw new IOException("Connect to repository folder failed"); - } - finally { - if (content != null) { - wrappedContent.release(content, this); - } - if (project != null) { - project.close(); - } - } + if (options.runScriptsNoImport) { + processNoImport(domainFolder.getPathname()); + } + else { + processWithImport(domainFolder.getPathname(), filesToImport); + } + } + finally { + if (project != null) { + project.close(); + } + } + } + + @Override + public void handleError(String title, String message, URL url, + IOException cause) throws IOException { + if (cause instanceof FileNotFoundException) { + throw new IOException("Connect to repository folder failed"); + } + if (cause != null) { + throw cause; + } + throw new IOException(title + ": " + message); + } + }, TaskMonitor.DUMMY); + + } + catch (CancelledException e) { + throw new IOException(e); // unexpected } finally { GhidraScriptUtil.dispose(); @@ -1835,17 +1837,16 @@ public class HeadlessAnalyzer { */ private static class HeadlessProject extends DefaultProject { - HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection) - throws IOException { - super(projectManager, connection); - AppInfo.setActiveProject(this); - } - HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator) throws NotOwnerException, LockException, IOException { super(projectManager, projectLocator, false); AppInfo.setActiveProject(this); } + + HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectData projectData) { + super(projectManager, (DefaultProjectData) projectData); + AppInfo.setActiveProject(this); + } } private static class HeadlessGhidraProjectManager extends DefaultProjectManager { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java index 37aa09d814..b885f7cd19 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java @@ -17,6 +17,7 @@ package ghidra.app.util.task; import java.io.IOException; import java.net.URL; +import java.util.concurrent.atomic.AtomicReference; import docking.widgets.OptionDialog; import ghidra.app.plugin.core.progmgr.ProgramLocator; @@ -25,9 +26,8 @@ import ghidra.framework.client.ClientUtil; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.main.AppInfo; import ghidra.framework.model.DomainFile; -import ghidra.framework.protocol.ghidra.GhidraURLConnection; -import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; -import ghidra.framework.protocol.ghidra.GhidraURLWrappedContent; +import ghidra.framework.protocol.ghidra.GhidraURLQuery; +import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter; import ghidra.framework.remote.User; import ghidra.framework.store.ExclusiveCheckoutException; import ghidra.program.model.lang.LanguageNotFoundException; @@ -35,6 +35,7 @@ import ghidra.program.model.listing.Program; import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; +import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; /** @@ -84,74 +85,42 @@ public class ProgramOpener { } /** - * Opens the program for the given location + * Opens the program for the given location. + * This method is intended to be invoked from within a {@link Task} or for headless operations. * @param locator the program location to open * @param monitor the TaskMonitor used for status and cancelling * @return the opened program or null if the operation failed or was cancelled */ public Program openProgram(ProgramLocator locator, TaskMonitor monitor) { if (locator.isURL()) { - return openURL(locator, monitor); + try { + return openURL(locator, monitor); + } + catch (CancelledException e) { + return null; + } + catch (IOException e) { + Msg.showError(this, null, "Program Open Failed", + "Failed to open Ghidra URL: " + locator.getURL(), e); + } } return openProgram(locator, locator.getDomainFile(), monitor); } - private Program openURL(ProgramLocator locator, TaskMonitor monitor) { - URL url = locator.getURL(); - GhidraURLWrappedContent wrappedContent = getWrappedContent(url); - if (wrappedContent == null) { - return null; - } - DomainFile remoteDomainFile = getDomainFile(url, wrappedContent); - if (remoteDomainFile == null) { - return null; - } + private Program openURL(ProgramLocator locator, TaskMonitor monitor) + throws CancelledException, IOException { + URL ghidraUrl = locator.getURL(); - try { - return openProgram(locator, remoteDomainFile, monitor); - } - finally { - wrappedContent.release(remoteDomainFile, this); - } - } - - private DomainFile getDomainFile(URL url, GhidraURLWrappedContent wrappedContent) { - try { - Object content = wrappedContent.getContent(this); - if (content instanceof DomainFile domainFile) { - return domainFile; + AtomicReference openedProgram = new AtomicReference<>(); + GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter() { + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor m) { + Program p = openProgram(locator, domainFile, m); // may return null + openedProgram.set(p); } - messageBadProgramURL(url); - if (content != null) { - wrappedContent.release(content, this); - } - } - catch (IOException e) { - Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url, - e); - } - return null; - } + }, monitor); - private GhidraURLWrappedContent getWrappedContent(URL url) { - try { - GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); - Object obj = c.getContent(); // read-only access - - if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { - return null; // assume user already notified - } - - if (obj instanceof GhidraURLWrappedContent wrappedContent) { - return wrappedContent; - } - return null; - } - catch (IOException e) { - Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url, - e); - } - return null; + return openedProgram.get(); } private Program openProgram(ProgramLocator locator, DomainFile domainFile, @@ -253,9 +222,8 @@ public class ProgramOpener { if (domainFile.checkout(dialog.exclusiveCheckout(), monitor)) { return; } - Msg.showError(this, null, "Checkout Failed", - "Exclusive checkout failed for: " + domainFile.getName() + - "\nOne or more users have file checked out!"); + Msg.showError(this, null, "Checkout Failed", "Exclusive checkout failed for: " + + domainFile.getName() + "\nOne or more users have file checked out!"); } catch (CancelledException e) { // we don't care, the task has been cancelled @@ -298,8 +266,4 @@ public class ProgramOpener { return option == OptionDialog.OPTION_ONE; } - private void messageBadProgramURL(URL url) { - Msg.error("Invalid Ghidra URL", "Ghidra URL does not reference a Ghidra Program: " + url); - } - } diff --git a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java index 1e78a4c725..b05357e063 100644 --- a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java +++ b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java @@ -69,7 +69,6 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest { layout = (GhidraApplicationLayout) createApplicationLayout(); ghidraGo = new GhidraGo(); - CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000; CheckForFileProcessedRunnable.MAX_WAIT_FOR_PROCESSING_MIN = 1; CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_PERIOD_MS = 10; @@ -129,7 +128,7 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest { } }); AbstractErrDialog err = waitForErrorDialog(); - assertEquals("Unsupported Content", err.getTitle()); + assertEquals("Content Not Found", err.getTitle()); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java index 829a85b5f3..ec3743ce55 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java @@ -19,13 +19,13 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import javax.swing.Icon; import generic.theme.GIcon; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.*; -import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.framework.store.FileSystem; import ghidra.framework.store.FolderItem; import ghidra.framework.store.local.LocalFileSystem; @@ -91,47 +91,56 @@ public abstract class LinkHandler extends DBCon return getObject(item, version, consumer, monitor, true); } - @SuppressWarnings("unchecked") private T getObject(FolderItem item, int version, Object consumer, TaskMonitor monitor, boolean immutable) throws IOException, VersionException, CancelledException { - URL url = getURL(item); + URL ghidraUrl = getURL(item); Class domainObjectClass = getDomainObjectClass(); if (domainObjectClass == null) { throw new UnsupportedOperationException(""); } - GhidraURLWrappedContent wrappedContent = null; - Object content = null; - final Object transientConsumer = new Object(); - try { - GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); - Object obj = c.getContent(); // read-only access - if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { + AtomicReference verExcRef = new AtomicReference<>(); + AtomicReference domainObjectRef = new AtomicReference<>(); + GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter(true) { + + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor m) + throws IOException, CancelledException { + if (!getDomainObjectClass().isAssignableFrom(domainFile.getDomainObjectClass())) { + throw new BadLinkException("Expected " + getDomainObjectClass() + + " but linked to " + domainFile.getDomainObjectClass()); + } + try { + @SuppressWarnings("unchecked") + T linkedObject = immutable + ? (T) domainFile.getImmutableDomainObject(consumer, version, monitor) + : (T) domainFile.getReadOnlyDomainObject(consumer, version, monitor); + domainObjectRef.set(linkedObject); + } + catch (VersionException e) { + verExcRef.set(e); + } + } + + @Override + public void handleUnauthorizedAccess(URL url) throws IOException { throw new IOException("Authorization failure"); } - if (!(obj instanceof GhidraURLWrappedContent)) { - throw new IOException("Unsupported linked content"); - } - wrappedContent = (GhidraURLWrappedContent) obj; - content = wrappedContent.getContent(transientConsumer); - if (!(content instanceof DomainFile)) { - throw new IOException("Unsupported linked content: " + content.getClass()); - } - DomainFile linkedFile = (DomainFile) content; - if (!getDomainObjectClass().isAssignableFrom(linkedFile.getDomainObjectClass())) { - throw new BadLinkException("Expected " + getDomainObjectClass() + - " but linked to " + linkedFile.getDomainObjectClass()); - } - return immutable ? (T) linkedFile.getImmutableDomainObject(consumer, version, monitor) - : (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor); + }, monitor); + + VersionException versionException = verExcRef.get(); + if (versionException != null) { + throw versionException; } - finally { - if (content != null) { - wrappedContent.release(content, transientConsumer); - } + + T domainObj = domainObjectRef.get(); + if (domainObj == null) { + throw new IOException( + "Failed to obtain linked object for unknown reason: " + item.getPathName()); } + return domainObj; } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index e0d7f24541..626c274921 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -120,22 +120,15 @@ public class DefaultProject implements Project { } /** - * Constructor for opening a URL-based project + * Construct a project with specific project manager and data * * @param projectManager the manager of this project - * @param connection project URL connection (not previously used) - * @throws IOException if I/O error occurs. + * @param projectData the project data */ - protected DefaultProject(DefaultProjectManager projectManager, GhidraURLConnection connection) - throws IOException { + protected DefaultProject(DefaultProjectManager projectManager, DefaultProjectData projectData) { this.projectManager = projectManager; - - Msg.info(this, "Opening project/repository: " + connection.getURL()); - projectData = (DefaultProjectData) connection.getProjectData(); - if (projectData == null) { - throw new IOException("Failed to open project/repository: " + connection.getURL()); - } + this.projectData = projectData; projectLocator = projectData.getProjectLocator(); if (!SystemUtilities.isInHeadlessMode()) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java index 4a32026162..d56d7db157 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java @@ -32,7 +32,7 @@ import ghidra.framework.main.FrontEndTool; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.preferences.Preferences; -import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask; +import ghidra.framework.protocol.ghidra.ContentTypeQueryTask; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.util.Msg; import ghidra.util.filechooser.GhidraFileChooserModel; @@ -272,7 +272,7 @@ class ToolServicesImpl implements ToolServices { } private String getContentType(URL url) throws IllegalArgumentException { - GetUrlContentTypeTask task = new GetUrlContentTypeTask(url); + ContentTypeQueryTask task = new ContentTypeQueryTask(url); TaskLauncher.launch(task); // blocking task return task.getContentType(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java new file mode 100644 index 0000000000..8861896dd7 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java @@ -0,0 +1,56 @@ +/* ### + * 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.protocol.ghidra; + +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.util.task.TaskMonitor; + +/** + * A blocking/modal Ghidra URL content type discovery task + */ +public class ContentTypeQueryTask extends GhidraURLQueryTask { + + private String contentType = "Unknown"; + + /** + * Construct a Ghidra URL content type query task + * @param ghidraUrl Ghidra URL (local or remote) + * @throws IllegalArgumentException if specified URL is not a Ghidra URL + * (see {@link GhidraURL}). + */ + public ContentTypeQueryTask(URL ghidraUrl) { + super("Query URL Content Type", ghidraUrl); + } + + /** + * Get the discovered content type (e.g., "Program") + * @return content type or null if error occured or unsupported URL content + * @throws IllegalStateException if task has not completed execution + */ + public String getContentType() { + if (!isDone()) { + throw new IllegalStateException("task has not completed"); + } + return contentType; + } + + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) { + contentType = domainFile.getContentType(); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java deleted file mode 100644 index 0e2e519d75..0000000000 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java +++ /dev/null @@ -1,113 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.framework.protocol.ghidra; - -import java.io.*; -import java.net.MalformedURLException; -import java.net.URL; - -import ghidra.framework.model.DomainFile; -import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; -import ghidra.util.Msg; -import ghidra.util.task.Task; -import ghidra.util.task.TaskMonitor; - -/** - * A blocking/modal Ghidra URL content type discovery task - */ -public class GetUrlContentTypeTask extends Task { - - private final URL ghidraUrl; - - private String contentType; - private boolean done = false; - - /** - * Construct a Ghidra URL content type discovery task - * @param ghidraUrl Ghidra URL (local or remote) - * @throws IllegalArgumentException if specified URL is not a Ghidra URL - * (see {@link GhidraURL}). - */ - public GetUrlContentTypeTask(URL ghidraUrl) { - super("Checking URL Content Type", true, false, true); - if (!GhidraURL.isLocalProjectURL(ghidraUrl) && - !GhidraURL.isServerRepositoryURL(ghidraUrl)) { - throw new IllegalArgumentException("unsupported URL"); - } - this.ghidraUrl = ghidraUrl; - } - - /** - * Get the discovered content type (e.g., "Program") - * @return content type or null if error occured or unsupported URL content - * @throws IllegalStateException if task has not completed execution - */ - public String getContentType() { - if (!done) { - throw new IllegalStateException("task has not completed"); - } - return contentType; - } - - @Override - public void run(TaskMonitor monitor) { - final Thread t = Thread.currentThread(); - monitor.addCancelledListener(() -> { - t.interrupt(); - }); - GhidraURLWrappedContent wrappedContent = null; - Object content = null; - try { - GhidraURLConnection c = (GhidraURLConnection) ghidraUrl.openConnection(); - Object obj = c.getContent(); // read-only access - if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { - return; // assume user already notified - } - if (obj instanceof GhidraURLWrappedContent) { - wrappedContent = (GhidraURLWrappedContent) obj; - content = wrappedContent.getContent(this); - } - if (!(content instanceof DomainFile)) { - Msg.showError(this, null, "Unsupported Content", - "Invalid project file URL: " + ghidraUrl); - return; - } - contentType = ((DomainFile) content).getContentType(); - } - catch (FileNotFoundException e) { - Msg.showError(this, null, "Content Not Found", e.getMessage()); - } - catch (MalformedURLException e) { - Msg.showError(this, null, "Invalid Ghidra URL", - "Improperly formed Ghidra URL: " + ghidraUrl); - } - catch (InterruptedIOException e) { - // ignore - assume cancelled - } - catch (IOException e) { - Msg.showError(this, null, "URL Access Failure", - "Failed to open Ghidra URL: " + e.getMessage()); - } - finally { - if (content != null) { - wrappedContent.release(content, this); - } - done = true; - } - } - - -} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java index 6989f1c4aa..1c09ae4d27 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java @@ -121,7 +121,8 @@ public class GhidraURL { * Confirm local project URL with {@link #isLocalProjectURL(URL)} prior to method use. * @param localProjectURL local Ghidra project URL * @return project locator or null if invalid path specified - * @throws IllegalArgumentException URL is not a valid local project URL + * @throws IllegalArgumentException URL is not a valid + * {@link #isLocalProjectURL(URL) local project URL}. */ public static ProjectLocator getProjectStorageLocator(URL localProjectURL) { if (!isLocalProjectURL(localProjectURL)) { @@ -330,9 +331,12 @@ public class GhidraURL { } /** - * Get normalized URL which corresponds to the local-project or repository + * Get Ghidra URL which corresponds to the local-project or repository with any + * file path or query details removed. * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted) * @return local-project or repository URL + * @throws IllegalArgumentException if URL does not specify the {@code ghidra} protocol + * or does not properly identify a remote repository or local project. */ public static URL getProjectURL(URL ghidraUrl) { if (!PROTOCOL.equals(ghidraUrl.getProtocol())) { @@ -456,6 +460,48 @@ public class GhidraURL { return host; } + /** + * Force the specified URL to specify a folder. This may be neccessary when only folders + * are supported since Ghidra permits both a folder and file to have the same name within + * its parent folder. This method simply ensures that the URL path ends with a {@code /} + * character if needed. + * @param ghidraUrl ghidra URL + * @return ghidra folder URL + * @throws IllegalArgumentException if specified URL is niether a + * {@link #isServerRepositoryURL(URL) valid remote server URL} + * or {@link #isLocalProjectURL(URL) local project URL}. + */ + public static URL getFolderURL(URL ghidraUrl) { + + if (!GhidraURL.isServerRepositoryURL(ghidraUrl) && + !GhidraURL.isLocalProjectURL(ghidraUrl)) { + throw new IllegalArgumentException("Invalid Ghidra URL: " + ghidraUrl); + } + + URL repoURL = GhidraURL.getProjectURL(ghidraUrl); + String path = GhidraURL.getProjectPathname(ghidraUrl); + + path = path.trim(); + if (!path.endsWith("/")) { + + // force explicit folder path + path += "/"; + + try { + if (GhidraURL.isServerRepositoryURL(ghidraUrl)) { + ghidraUrl = new URL(repoURL + path); + } + else { + ghidraUrl = new URL(repoURL + "?" + path); + } + } + catch (MalformedURLException e) { + throw new AssertionError(e); + } + } + return ghidraUrl; + } + /** * Get a normalized URL which eliminates use of host names and optional URL ref * which may prevent direct comparison. @@ -634,7 +680,7 @@ public class GhidraURL { * @param ref optional URL ref or null * Folder paths should end with a '/' character. * @return Ghidra Server repository content URL - * @throws IllegalArgumentException if invalid arguments are specified + * @throws IllegalArgumentException if required arguments are blank or invalid */ public static URL makeURL(String host, int port, String repositoryName, String repositoryFolderPath, String fileName, String ref) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java new file mode 100644 index 0000000000..2778a9e848 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java @@ -0,0 +1,157 @@ +/* ### + * 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.protocol.ghidra; + +import java.io.IOException; +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; + +/** + * {@link GhidraURLQuery} performs remote Ghidra repository and read-only local project + * queries for processing either a {@link DomainFile} or {@link DomainFolder} that a + * Ghidra URL may reference. + */ +public abstract class GhidraURLQuery { + + /** + * Perform read-only query using specified GhidraURL and process result. + * Both local project and remote repository URLs are supported. + * This method is intended to be invoked from within a {@link Task} or for headless operations. + * @param ghidraUrl local or remote Ghidra URL + * @param resultHandler query result handler + * @param monitor task monitor + * @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler} + * @throws CancelledException if task is cancelled + */ + public static void queryUrl(URL ghidraUrl, GhidraURLResultHandler resultHandler, + TaskMonitor monitor) throws IOException, CancelledException { + doQueryUrl(ghidraUrl, true, resultHandler, monitor); + } + + /** + * Perform query using specified GhidraURL and process result. + * Both local project and remote repository URLs are supported. + * This method is intended to be invoked from within a {@link Task} or for headless operations. + * @param ghidraUrl local or remote Ghidra URL + * @param readOnly allows update/commit (false) or read-only (true) access. + * @param resultHandler query result handler + * @param monitor task monitor + * @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler} + * @throws CancelledException if task is cancelled + */ + public static void queryRepositoryUrl(URL ghidraUrl, boolean readOnly, + GhidraURLResultHandler resultHandler, TaskMonitor monitor) + throws IOException, CancelledException { + if (!GhidraURL.isServerRepositoryURL(ghidraUrl)) { + throw new IllegalArgumentException("Unsupported repository URL: " + ghidraUrl); + } + doQueryUrl(ghidraUrl, readOnly, resultHandler, monitor); + } + + private static void doQueryUrl(URL ghidraUrl, boolean readOnly, + GhidraURLResultHandler resultHandler, TaskMonitor monitor) + throws IOException, CancelledException { + + GhidraURLConnection c; + Object obj = null; + StatusCode status = null; + try { + c = (GhidraURLConnection) ghidraUrl.openConnection(); + c.setReadOnly(readOnly); // writable repository connection + obj = c.getContent(); // read-only access + status = c.getStatusCode(); + } + catch (IOException e) { + resultHandler.handleError("URL Connection Error", e.getMessage(), ghidraUrl, e); + } + + GhidraURLWrappedContent wrappedContent = null; + Object content = null; + try { + IOException generatedErr = null; + switch (status) { + case OK: + break; + + case UNAUTHORIZED: + resultHandler.handleUnauthorizedAccess(ghidraUrl); + return; + + case NOT_FOUND: + generatedErr = new IOException("Project or repository not found"); + break; + + case LOCKED: + // Local projects are only accessed read-only, this condition should not occur + throw new AssertionError("Unexpected local project lock condition"); + + case UNAVAILABLE: + generatedErr = + new IOException("Server connection error occured (see log files)"); + break; + + default: + } + + if (generatedErr != null) { + resultHandler.handleError("Content Not Found", generatedErr.getMessage(), ghidraUrl, + generatedErr); + return; + } + + if (!(obj instanceof GhidraURLWrappedContent)) { + resultHandler.handleError("Unsupported Content", + "URL does not correspond to a file or folder", null, null); + return; + } + + wrappedContent = (GhidraURLWrappedContent) obj; + try { + content = wrappedContent.getContent(resultHandler); + } + catch (IOException e) { + resultHandler.handleError("Content Not Found", e.getMessage(), null, e); + return; + } + + monitor.checkCancelled(); + if (content instanceof DomainFile file) { + resultHandler.processResult(file, ghidraUrl, monitor); + } + else if (content instanceof DomainFolder folder) { + resultHandler.processResult(folder, ghidraUrl, monitor); + } + else { + // unexpected condition + resultHandler.handleError("Unsupported Content", + "Content class: " + content.getClass().getName(), ghidraUrl, null); + } + } + finally { + if (content != null) { + wrappedContent.release(content, resultHandler); + } + monitor.checkCancelled(); + } + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java new file mode 100644 index 0000000000..9bcb535399 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java @@ -0,0 +1,111 @@ +/* ### + * 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.protocol.ghidra; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; + +/** + * {@link GhidraURLQueryTask} provides an abstract Task which performs remote Ghidra + * repository and read-only local project queries for processing either a {@link DomainFile} + * or {@link DomainFolder} that a Ghidra URL may reference. + *

+ * All implementations of this Task should override one or + * both of the processing methods {@link #processResult(DomainFile, URL, TaskMonitor)} + * and {@link #processResult(DomainFolder, URL, TaskMonitor)}. For any process method + * not overriden the default behavior is reporting Unsupported Content. + *

+ * If {@link #handleError(String, String, URL, IOException)} + * is not overriden all errors are reported via + * {@link Msg#showError(Object, java.awt.Component, String, Object)}. + */ +public abstract class GhidraURLQueryTask extends Task implements GhidraURLResultHandler { + + private final URL ghidraUrl; + + private boolean done = false; + + /** + * Construct a Ghidra URL read-only query task. + * @param title task dialog title + * @param ghidraUrl Ghidra URL (local or remote) + * @throws IllegalArgumentException if specified URL is not a Ghidra URL + * (see {@link GhidraURL}). + */ + protected GhidraURLQueryTask(String title, URL ghidraUrl) { + super(title, true, false, true); + if (!GhidraURL.isLocalProjectURL(ghidraUrl) && + !GhidraURL.isServerRepositoryURL(ghidraUrl)) { + throw new IllegalArgumentException("Unsupported URL: " + ghidraUrl); + } + this.ghidraUrl = ghidraUrl; + } + + /** + * Determine if the task has completed its execution + * @return true if done executing else false + */ + protected boolean isDone() { + return done; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + final Thread t = Thread.currentThread(); + CancelledListener cancelledListener = () -> t.interrupt(); + monitor.addCancelledListener(cancelledListener); + + try { + GhidraURLQuery.queryUrl(ghidraUrl, this, monitor); + } + catch (InterruptedIOException e) { + // ignore - assume cancelled + } + catch (IOException e) { + handleError("URL Access Failure", e.getMessage(), ghidraUrl, e); + } + finally { + monitor.removeCancelledListener(cancelledListener); + monitor.checkCancelled(); + done = true; + } + } + + @Override + public void handleError(String title, String message, URL url, IOException cause) { + Msg.showError(GhidraURLQuery.class, null, title, message + ":\n" + url); + } + + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) + throws IOException { + handleError("Unsupported Content", "File URL: " + url, null, null); + } + + @Override + public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor) + throws IOException { + handleError("Unsupported Content", "Folder URL: " + url, null, null); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java new file mode 100644 index 0000000000..61e579318e --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java @@ -0,0 +1,79 @@ +/* ### + * 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.protocol.ghidra; + +import java.io.IOException; +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public interface GhidraURLResultHandler { + + /** + * Process the specified {@code domainFile} query result. + * Dissemination of the {@code domainFile} instance should be restricted and any use of it + * completed before the call to this method returns. Upon return from this method call the + * underlying connection will be closed and at which time the {@code domainFile} instance + * will become invalid. + * @param domainFile {@link DomainFile} to which the URL refers. + * @param url URL which was used to retrieve the specified {@code domainFile} + * @param monitor task monitor + * @throws IOException if an IO error occurs + * @throws CancelledException if task is cancelled + */ + void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) + throws IOException, CancelledException; + + /** + * Process the specified {@code domainFolder} query result. + * Dissemination of the {@code domainFolder} instance should be restricted and any use of it + * completed before the call to this method returns. Upon return from this method call the + * underlying connection will be closed and at which time the {@code domainFolder} instance + * will become invalid. + * @param domainFolder {@link DomainFolder} to which the URL refers. + * @param url URL which was used to retrieve the specified {@code domainFolder} + * @param monitor task monitor + * @throws IOException if an IO error occurs + * @throws CancelledException if task is cancelled + */ + void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor) + throws IOException, CancelledException; + + /** + * Handle error which occurs during query operation. + * @param title error title + * @param message error detail + * @param url URL which was used for query + * @param cause cause of error (may be null) + * @throws IOException may be thrown if handler decides to propogate error + */ + void handleError(String title, String message, URL url, IOException cause) throws IOException; + + /** + * Handle authorization error. + * This condition is generally logged and user notified via GUI during connection processing. + * This method does not do anything by default but is provided to flag failure if needed since + * {@link #handleError(String, String, URL, IOException)} will not be invoked. + * @param url connection URL + * @throws IOException may be thrown if handler decides to propogate error + */ + default void handleUnauthorizedAccess(URL url) throws IOException { + // do nothing - assume user has already been notified or issue has been logged + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandlerAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandlerAdapter.java new file mode 100644 index 0000000000..d70de5a294 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandlerAdapter.java @@ -0,0 +1,81 @@ +/* ### + * 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.protocol.ghidra; + +import java.io.IOException; +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link GhidraURLResultHandlerAdapter} provides a basic result handler for + * {@link GhidraURLQuery}. All uses of this adapter should override one or + * both of the processing methods {@link #processResult(DomainFile, URL, TaskMonitor)} + * and {@link #processResult(DomainFolder, URL, TaskMonitor)}. For any process method + * not overriden the default behavior is reporting Unsupported Content. + */ +public class GhidraURLResultHandlerAdapter implements GhidraURLResultHandler { + + private final boolean throwErrorByDefault; + + /** + * Construct adapter. If {@link #handleError(String, String, URL, IOException)} + * is not overriden all errors are reported via + * {@link Msg#showError(Object, java.awt.Component, String, Object)}. + */ + public GhidraURLResultHandlerAdapter() { + throwErrorByDefault = false; + } + + /** + * Construct adapter with preferred error handling. There is no need to use this constructor + * if {@link #handleError(String, String, URL, IOException)} is override. + * @param throwErrorByDefault if true all errors will be thrown as an {@link IOException}, + * otherwise error is reported via {@link Msg#showError(Object, java.awt.Component, String, Object)}. + */ + public GhidraURLResultHandlerAdapter(boolean throwErrorByDefault) { + this.throwErrorByDefault = throwErrorByDefault; + } + + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) + throws IOException, CancelledException { + handleError("Unsupported Content", "File URL: " + url, null, null); + } + + @Override + public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor) + throws IOException, CancelledException { + handleError("Unsupported Content", "Folder URL: " + url, null, null); + } + + @Override + public void handleError(String title, String message, URL url, IOException cause) + throws IOException { + if (!throwErrorByDefault) { + Msg.showError(GhidraURLQuery.class, null, title, message + ":\n" + url); + } + if (cause != null) { + throw cause; + } + throw new IOException(title + ": " + message); + } + +}