diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java index 1cf6c60f5f..0b794e24cb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java @@ -540,7 +540,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { this.program = p; this.domainFile = domainFile; if (domainFile instanceof LinkedDomainFile linkedDomainFile) { - this.ghidraURL = linkedDomainFile.getSharedProjectURL(); + this.ghidraURL = linkedDomainFile.getSharedProjectURL(null); } else { this.ghidraURL = null; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/GhidraFileTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/GhidraFileTest.java index 4fab08ac11..0798cbb592 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/GhidraFileTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/GhidraFileTest.java @@ -27,6 +27,7 @@ import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.*; import ghidra.framework.store.local.LocalFileSystem; import ghidra.test.AbstractGhidraHeadedIntegrationTest; @@ -86,6 +87,15 @@ public class GhidraFileTest extends AbstractGhidraHeadedIntegrationTest { deleteAll(sharedProjectDir); } + @Test + public void testLocalURL() throws IOException { + createDB(privateFS, "/a", "file1"); + assertEquals(GhidraURL.makeURL(pfm.getProjectLocator(), "/a/file1", "xyz"), + pfm.getFile("/a/file1").getLocalProjectURL("xyz")); + assertEquals(GhidraURL.makeURL(pfm.getProjectLocator(), "/a/file1", null), + pfm.getFile("/a/file1").getLocalProjectURL(null)); + } + @Test public void testFileID() throws IOException { createDB(privateFS, "/a", "file1"); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index 0bb4d082bf..dd71ba1b10 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -23,6 +23,8 @@ import java.util.*; import javax.swing.Icon; +import org.apache.commons.lang3.StringUtils; + import ghidra.framework.client.*; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; @@ -116,16 +118,13 @@ public class DomainFileProxy implements DomainFile { return parentPath + DomainFolder.SEPARATOR + getName(); } - private URL getSharedFileURL(URL sharedProjectURL) { + private URL getSharedFileURL(URL sharedProjectURL, String ref) { try { - // Direct URL construction done so that ghidra protocol - // extension may be supported - String urlStr = sharedProjectURL.toExternalForm(); - if (urlStr.endsWith("/")) { - urlStr = urlStr.substring(0, urlStr.length() - 1); + String spec = getPathname().substring(1); // remove leading '/' + if (!StringUtils.isEmpty(ref)) { + spec += "#" + ref; } - urlStr += getPathname(); - return new URL(urlStr); + return new URL(sharedProjectURL, spec); } catch (MalformedURLException e) { // ignore @@ -133,7 +132,7 @@ public class DomainFileProxy implements DomainFile { return null; } - private URL getSharedFileURL(Properties properties) { + private URL getSharedFileURL(Properties properties, String ref) { if (properties == null) { return null; } @@ -166,9 +165,8 @@ public class DomainFileProxy implements DomainFile { return null; } ServerInfo serverInfo = repository.getServerInfo(); - return GhidraURL.makeURL(serverInfo.getServerName(), - serverInfo.getPortNumber(), repository.getName(), - item.getPathName()); + return GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(), + repository.getName(), item.getPathName(), ref); } catch (IOException e) { // ignore @@ -182,15 +180,27 @@ public class DomainFileProxy implements DomainFile { } @Override - public URL getSharedProjectURL() { + public URL getSharedProjectURL(String ref) { if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) { URL projectURL = projectLocation.getURL(); if (GhidraURL.isServerRepositoryURL(projectURL)) { - return getSharedFileURL(projectURL); + return getSharedFileURL(projectURL, ref); } Properties properties = ProjectFileManager.readProjectProperties(projectLocation.getProjectDir()); - return getSharedFileURL(properties); + return getSharedFileURL(properties, ref); + } + return null; + } + + @Override + public URL getLocalProjectURL(String ref) { + if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) { + URL projectURL = projectLocation.getURL(); + if (GhidraURL.isServerRepositoryURL(projectURL)) { + return null; + } + return GhidraURL.makeURL(projectLocation, getPathname(), ref); } return null; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java index b784e944e2..30034fc122 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java @@ -124,9 +124,20 @@ public class GhidraFile implements DomainFile { } @Override - public URL getSharedProjectURL() { + public URL getSharedProjectURL(String ref) { try { - return getFileData().getSharedProjectURL(); + return getFileData().getSharedProjectURL(ref); + } + catch (IOException e) { + // ignore + } + return null; + } + + @Override + public URL getLocalProjectURL(String ref) { + try { + return getFileData().getLocalProjectURL(ref); } catch (IOException e) { // ignore @@ -456,10 +467,9 @@ public class GhidraFile implements DomainFile { } @Override - public boolean checkout(boolean exclusive, TaskMonitor monitor) throws IOException, - CancelledException { - return getFileData().checkout(exclusive, - monitor != null ? monitor : TaskMonitor.DUMMY); + public boolean checkout(boolean exclusive, TaskMonitor monitor) + throws IOException, CancelledException { + return getFileData().checkout(exclusive, monitor != null ? monitor : TaskMonitor.DUMMY); } @Override @@ -470,10 +480,9 @@ public class GhidraFile implements DomainFile { } @Override - public void merge(boolean okToUpgrade, TaskMonitor monitor) throws IOException, - VersionException, CancelledException { - getFileData().merge(okToUpgrade, - monitor != null ? monitor : TaskMonitor.DUMMY); + public void merge(boolean okToUpgrade, TaskMonitor monitor) + throws IOException, VersionException, CancelledException { + getFileData().merge(okToUpgrade, monitor != null ? monitor : TaskMonitor.DUMMY); } @Override @@ -521,8 +530,8 @@ public class GhidraFile implements DomainFile { } @Override - public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, - CancelledException { + public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) + throws IOException, CancelledException { if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { throw new UnsupportedOperationException("newParent does not support copyTo"); } @@ -559,8 +568,7 @@ public class GhidraFile implements DomainFile { * @throws CancelledException if task is cancelled */ void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException { - getFileData().convertToPrivateFile( - monitor != null ? monitor : TaskMonitor.DUMMY); + getFileData().convertToPrivateFile(monitor != null ? monitor : TaskMonitor.DUMMY); } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java index eccac2f43c..370cb130e8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java @@ -24,6 +24,8 @@ import java.util.Map; import javax.swing.Icon; +import org.apache.commons.lang3.StringUtils; + import db.DBHandle; import db.Field; import db.buffers.*; @@ -209,17 +211,21 @@ public class GhidraFileData { /** * Get a remote Ghidra URL for this domain file if available within a remote repository. + * @param ref reference within a file, may be null. NOTE: such reference interpretation + * is specific to a domain object and tooling with limited support. * @return remote Ghidra URL for this file or null */ - URL getSharedProjectURL() { + URL getSharedProjectURL(String ref) { synchronized (fileSystem) { RepositoryAdapter repository = parent.getProjectFileManager().getRepository(); if (versionedFolderItem != null && repository != null) { URL folderURL = parent.getDomainFolder().getSharedProjectURL(); try { - // Direct URL construction done so that ghidra protocol - // extension may be supported - return new URL(folderURL.toExternalForm() + name); + String spec = name; + if (!StringUtils.isEmpty(ref)) { + spec += "#" + ref; + } + return new URL(folderURL, spec); } catch (MalformedURLException e) { // ignore @@ -229,6 +235,23 @@ public class GhidraFileData { } } + /** + * Get a local Ghidra URL for this domain file if available within a non-transient local + * project. A null value is returned for a transient project. + * @param ref reference within a file, may be null. NOTE: such reference interpretation + * is specific to a domain object and tooling with limited support. + * @return local Ghidra URL for this file or null if transient or not applicable + */ + URL getLocalProjectURL(String ref) { + synchronized (fileSystem) { + ProjectLocator projectLocator = parent.getProjectLocator(); + if (!projectLocator.isTransient()) { + return GhidraURL.makeURL(projectLocator, getPathname(), ref); + } + return null; + } + } + /** * Reassign a new file-ID to resolve file-ID conflict. * Conflicts can occur as a result of a cancelled check-out. @@ -453,8 +476,7 @@ public class GhidraFileData { return true; } DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname()); - if (!(dobj instanceof DomainObjectAdapterDB) || - !dobj.isChanged()) { + if (!(dobj instanceof DomainObjectAdapterDB) || !dobj.isChanged()) { return true; } LockingTaskMonitor monitor = null; @@ -881,8 +903,8 @@ public class GhidraFileData { : CheckoutType.NORMAL; } ItemCheckoutStatus checkout = - versionedFolderItem.checkout(checkoutType, user, ItemCheckoutStatus.getProjectPath( - projectLocator.toString(), projectLocator.isTransient())); + versionedFolderItem.checkout(checkoutType, user, ItemCheckoutStatus + .getProjectPath(projectLocator.toString(), projectLocator.isTransient())); if (checkout == null) { return false; } @@ -1690,10 +1712,8 @@ public class GhidraFileData { BufferFile bufferFile = ((DatabaseItem) item).open(); try { newParentData.getLocalFileSystem() - .createDatabase(pathname, targetName, - FileIDFactory.createFileID(), bufferFile, null, contentType, - true, - monitor, user); + .createDatabase(pathname, targetName, FileIDFactory.createFileID(), + bufferFile, null, contentType, true, monitor, user); } finally { bufferFile.dispose(); @@ -1703,8 +1723,8 @@ public class GhidraFileData { InputStream istream = ((DataFileItem) item).getInputStream(); try { newParentData.getLocalFileSystem() - .createDataFile(pathname, targetName, - istream, null, contentType, monitor); + .createDataFile(pathname, targetName, istream, null, contentType, + monitor); } finally { istream.close(); @@ -1745,10 +1765,8 @@ public class GhidraFileData { } try { destFolderData.getLocalFileSystem() - .createDatabase(pathname, targetName, - FileIDFactory.createFileID(), bufferFile, null, contentType, true, - monitor, - user); + .createDatabase(pathname, targetName, FileIDFactory.createFileID(), + bufferFile, null, contentType, true, monitor, user); } finally { bufferFile.dispose(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java index 3a3de7ff1e..360523d0cb 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java @@ -199,8 +199,7 @@ public class GhidraFolder implements DomainFolder { repository.getName()); } try { - // Direct URL construction done so that ghidra protocol - // extension may be supported + // Direct URL construction done so that ghidra protocol extension may be supported String urlStr = projectURL.toExternalForm(); if (urlStr.endsWith(FileSystem.SEPARATOR)) { urlStr = urlStr.substring(0, urlStr.length() - 1); @@ -217,6 +216,15 @@ public class GhidraFolder implements DomainFolder { } } + @Override + public URL getLocalProjectURL() { + ProjectLocator projectLocator = parent.getProjectLocator(); + if (!projectLocator.isTransient()) { + return GhidraURL.makeURL(projectLocator, getPathname(), null); + } + return null; + } + @Override public boolean isInWritableProject() { return !getProjectData().getLocalFileSystem().isReadOnly(); 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 644d140931..829a85b5f3 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 @@ -43,7 +43,7 @@ import ghidra.util.task.TaskMonitor; * @param {@link URLLinkObject} implementation class */ public abstract class LinkHandler extends DBContentHandler { - + public static final String URL_METADATA_KEY = "link.url"; // 16x16 link icon where link is placed in lower-left corner @@ -93,11 +93,10 @@ public abstract class LinkHandler extends DBCon @SuppressWarnings("unchecked") private T getObject(FolderItem item, int version, Object consumer, TaskMonitor monitor, - boolean immutable) - throws IOException, VersionException, CancelledException { + boolean immutable) throws IOException, VersionException, CancelledException { URL url = getURL(item); - + Class domainObjectClass = getDomainObjectClass(); if (domainObjectClass == null) { throw new UnsupportedOperationException(""); @@ -105,6 +104,7 @@ public abstract class LinkHandler extends DBCon GhidraURLWrappedContent wrappedContent = null; Object content = null; + final Object transientConsumer = new Object(); try { GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); Object obj = c.getContent(); // read-only access @@ -115,22 +115,21 @@ public abstract class LinkHandler extends DBCon throw new IOException("Unsupported linked content"); } wrappedContent = (GhidraURLWrappedContent) obj; - content = wrappedContent.getContent(consumer); + 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()); + throw new BadLinkException("Expected " + getDomainObjectClass() + + " but linked to " + linkedFile.getDomainObjectClass()); } return immutable ? (T) linkedFile.getImmutableDomainObject(consumer, version, monitor) : (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor); } finally { if (content != null) { - wrappedContent.release(content, consumer); + wrappedContent.release(content, transientConsumer); } } } @@ -151,8 +150,7 @@ public abstract class LinkHandler extends DBCon @Override public final DomainObjectMergeManager getMergeManager(DomainObject resultsObj, - DomainObject sourceObj, - DomainObject originalObj, DomainObject latestObj) { + DomainObject sourceObj, DomainObject originalObj, DomainObject latestObj) { return null; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java index 848f89c494..21e155ae0d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java @@ -25,6 +25,8 @@ import java.util.Map; import javax.help.UnsupportedOperationException; import javax.swing.Icon; +import org.apache.commons.lang3.StringUtils; + import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.*; @@ -100,13 +102,15 @@ class LinkedGhidraFile implements LinkedDomainFile { } @Override - public URL getSharedProjectURL() { + public URL getSharedProjectURL(String ref) { URL folderURL = parent.getSharedProjectURL(); if (GhidraURL.isServerRepositoryURL(folderURL)) { - // Direct URL construction done so that ghidra protocol - // extension may be supported try { - return new URL(folderURL.toExternalForm() + fileName); + String spec = fileName; + if (!StringUtils.isEmpty(ref)) { + spec += "#" + ref; + } + return new URL(folderURL, spec); } catch (MalformedURLException e) { // ignore @@ -115,6 +119,15 @@ class LinkedGhidraFile implements LinkedDomainFile { return null; } + @Override + public URL getLocalProjectURL(String ref) { + ProjectLocator projectLocator = parent.getProjectLocator(); + if (!projectLocator.isTransient()) { + return GhidraURL.makeURL(projectLocator, getPathname(), ref); + } + return null; + } + @Override public ProjectLocator getProjectLocator() { return parent.getProjectLocator(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java index e2577b2da8..1ac8db6833 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java @@ -107,6 +107,15 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return null; } + @Override + public URL getLocalProjectURL() { + ProjectLocator projectLocator = parent.getProjectLocator(); + if (!projectLocator.isTransient()) { + return GhidraURL.makeURL(projectLocator, getPathname(), null); + } + return null; + } + @Override public ProjectLocator getProjectLocator() { return parent.getProjectLocator(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java index 77eb2bc299..9c2eac2aa7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java @@ -89,12 +89,23 @@ public interface DomainFile extends Comparable { public String getPathname(); /** - * Get a remote Ghidra URL for this domain file if available within the associated shared + * Get a remote Ghidra URL for this domain file if available within an associated shared * project repository. A null value will be returned if shared file does not exist and - * may be returned if shared repository is not connected or a connection error occurs. + * may also be returned if shared repository is not connected or a connection error occurs. + * @param ref reference within a file, may be null. NOTE: such reference interpretation + * is specific to a domain object and tooling with limited support. * @return remote Ghidra URL for this file or null */ - public URL getSharedProjectURL(); + public URL getSharedProjectURL(String ref); + + /** + * Get a local Ghidra URL for this domain file if available within the associated non-transient + * local project. A null value will be returned if project is transient. + * @param ref reference within a file, may be null. NOTE: such reference interpretation + * is specific to a domain object and tooling with limited support. + * @return local Ghidra URL for this file or null if transient or not applicable + */ + public URL getLocalProjectURL(String ref); /** * Returns the local storage location for the project that this DomainFile belongs to. diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java index 18ced6cd51..583e8ad5e8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java @@ -35,8 +35,7 @@ import ghidra.util.task.TaskMonitor; */ public interface DomainFolder extends Comparable { - public static final Icon OPEN_FOLDER_ICON = - new GIcon("icon.datatree.node.domain.folder.open"); + public static final Icon OPEN_FOLDER_ICON = new GIcon("icon.datatree.node.domain.folder.open"); public static final Icon CLOSED_FOLDER_ICON = new GIcon("icon.datatree.node.domain.folder.closed"); @@ -90,13 +89,21 @@ public interface DomainFolder extends Comparable { public String getPathname(); /** - * Get a remote Ghidra URL for this domain folder within the associated shared - * project repository. URL path will end with "/". A null value will be returned if not - * associated with a shared project. + * Get a remote Ghidra URL for this domain folder if available within an associated shared + * project repository. URL path will end with "/". A null value will be returned if shared + * folder does not exist and may also be returned if shared repository is not connected or a + * connection error occurs. * @return remote Ghidra URL for this folder or null */ public URL getSharedProjectURL(); + /** + * Get a local Ghidra URL for this domain file if available within the associated non-transient + * local project. A null value will be returned if project is transient. + * @return local Ghidra URL for this folder or null if transient or not applicable + */ + public URL getLocalProjectURL(); + /** * Returns true if this file is in a writable project. * @return true if writable @@ -219,8 +226,8 @@ public interface DomainFolder extends Comparable { * @throws IOException thrown if an IO or access error occurs. * @throws CancelledException if task monitor cancelled operation. */ - public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, - CancelledException; + public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) + throws IOException, CancelledException; /** * Copy this folder into the newParent folder as a link file. Restrictions: diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java index 5e4ec72188..1be5916211 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java @@ -102,7 +102,12 @@ public class TestDummyDomainFile implements DomainFile { } @Override - public URL getSharedProjectURL() { + public URL getSharedProjectURL(String ref) { + throw new UnsupportedOperationException(); + } + + @Override + public URL getLocalProjectURL(String ref) { throw new UnsupportedOperationException(); } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java index df115ddac6..82052b474e 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java @@ -88,6 +88,11 @@ public class TestDummyDomainFolder implements DomainFolder { throw new UnsupportedOperationException(); } + @Override + public URL getLocalProjectURL() { + throw new UnsupportedOperationException(); + } + @Override public boolean isInWritableProject() { throw new UnsupportedOperationException();