Merge remote-tracking branch 'origin/GP-5907-5908_ghidra1_ProjectTreeUpdateAndDelete--SQUASHED'

This commit is contained in:
ghidra1 2025-09-12 15:35:03 -04:00
commit f6148f063a
38 changed files with 1988 additions and 515 deletions

View file

@ -115,7 +115,9 @@
"help/topics/VersionControl/project_repository.htm#SampleCheckOutIcon">checked out</A>. "help/topics/VersionControl/project_repository.htm#SampleCheckOutIcon">checked out</A>.
In addition, unique icons are used to reflect content-type and if it corresponds to In addition, unique icons are used to reflect content-type and if it corresponds to
a link-file referring to another file or folder (see <A href="#Paste_Link">creating links</A>). a link-file referring to another file or folder (see <A href="#Paste_Link">creating links</A>).
Open this view by activating the project window "Tree View" tab.</P> Open this view by activating the project window "Tree View" tab. Within the tree view
internally linked-folders may be expanded to reveal the linked content which corresponds
to another folder within the project.</P>
<P><IMG src="help/shared/tip.png" border="0">Although Ghidra allows a folder and file within <P><IMG src="help/shared/tip.png" border="0">Although Ghidra allows a folder and file within
the same parent folder to have the same name, it is recommended this be avoided if possible. the same parent folder to have the same name, it is recommended this be avoided if possible.
@ -313,6 +315,26 @@
project view once opened.</LI> project view once opened.</LI>
</OL> </OL>
</BLOCKQUOTE>
<H4><A name="Select_Real_File_or_Folder"></A>Select Real File or Folder</H4>
<BLOCKQUOTE>
<P>Select a folder or file tree node from an expanded linked-folder or sub-folder
node. Content is considered linked if one of its parent nodes corresponds to an
expanded folder-link. This linked-content corresponds to a real file or folder
contained within another folder. The ability to select the real file or folder
may be useful when trying to understand the true origin of such content since this
path is not displayed.
</P>
<OL>
<LI>
Select a folder or file tree node from an expanded linked-folder or linked-sub-folder
node, right mouse click and choose the <I>Select Real File</I> or <I>Select Real Folder</I>
option. The real file or folder will be selected within the tree if possible.</LI>
</OL>
</BLOCKQUOTE> </BLOCKQUOTE>
@ -656,7 +678,8 @@
See <A href="#GhidraURLFormats">Ghidra URL formats</A> below. See <A href="#GhidraURLFormats">Ghidra URL formats</A> below.
Since a folder link is stored as a file, it may appear with various icon states which Since a folder link is stored as a file, it may appear with various icon states which
correspond to version control. Folder links only support a single version and may not correspond to version control. Folder links only support a single version and may not
be modified. be modified. The tree may permit expanding such nodes to reveal their linked-content
as files and sub-folders.
</TD> </TD>
</TR> </TR>

View file

@ -85,7 +85,7 @@ public class ProgramLocator {
try { try {
// Attempt to resolve to actual linked-file to allow for // Attempt to resolve to actual linked-file to allow for
// direct URL reference // direct URL reference
domainFile = linkedFile.getLinkedFile(); domainFile = linkedFile.getRealFile();
} }
catch (IOException e) { catch (IOException e) {
Msg.error(this, "Failed to resolve linked-file", e); Msg.error(this, "Failed to resolve linked-file", e);

View file

@ -18,14 +18,17 @@ package ghidra.app.util.task;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import ghidra.app.plugin.core.progmgr.ProgramLocator; import ghidra.app.plugin.core.progmgr.ProgramLocator;
import ghidra.app.util.dialog.CheckoutDialog; import ghidra.app.util.dialog.CheckoutDialog;
import ghidra.framework.client.ClientUtil; import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.data.LinkHandler.LinkStatus;
import ghidra.framework.main.AppInfo; import ghidra.framework.main.AppInfo;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.model.LinkFileInfo;
import ghidra.framework.protocol.ghidra.GhidraURLQuery; import ghidra.framework.protocol.ghidra.GhidraURLQuery;
import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter; import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter;
@ -142,6 +145,18 @@ public class ProgramOpener {
} }
private Program openNormal(DomainFile domainFile, TaskMonitor monitor) { private Program openNormal(DomainFile domainFile, TaskMonitor monitor) {
LinkFileInfo linkInfo = domainFile.getLinkInfo();
if (linkInfo != null) {
StringBuilder buf = new StringBuilder();
LinkStatus linkStatus = linkInfo.getLinkStatus(m -> buf.append(m));
if (linkStatus == LinkStatus.BROKEN) {
Msg.showError(this, null, "Error Opening " + domainFile.getName(),
"Failed to open Program Link " + domainFile.getPathname() + "\n" + buf);
return null;
}
}
String filename = domainFile.getName(); String filename = domainFile.getName();
performOptionalCheckout(domainFile, monitor); performOptionalCheckout(domainFile, monitor);
try { try {

View file

@ -24,7 +24,8 @@ import org.apache.logging.log4j.Logger;
import db.buffers.LocalManagedBufferFile; import db.buffers.LocalManagedBufferFile;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.framework.store.local.*; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.framework.store.local.LocalFolderItem;
import ghidra.server.Repository; import ghidra.server.Repository;
import ghidra.server.RepositoryManager; import ghidra.server.RepositoryManager;
import ghidra.util.InvalidNameException; import ghidra.util.InvalidNameException;
@ -282,11 +283,8 @@ public class RepositoryFolder {
throw new DuplicateFileException(itemName + " already exists"); throw new DuplicateFileException(itemName + " already exists");
} }
LocalTextDataItem textDataItem = fileSystem.createTextDataItem(getPathname(), itemName, fileSystem.createTextDataItem(getPathname(), itemName, fileID, contentType, textData,
fileID, contentType, textData, null); // comment conveyed with Version info below comment, user);
Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
textDataItem.setVersionInfo(singleVersion);
RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName); RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
fileMap.put(itemName, rf); fileMap.put(itemName, rf);

View file

@ -211,13 +211,14 @@ public interface FileSystem {
* @param contentType application defined content type * @param contentType application defined content type
* @param textData text data (required) * @param textData text data (required)
* @param comment file comment (may be null, only used if versioning is enabled) * @param comment file comment (may be null, only used if versioning is enabled)
* @param user name of user creating item (required for local versioned item)
* @return new data file * @return new data file
* @throws DuplicateFileException Thrown if a folderItem with that name already exists. * @throws DuplicateFileException Thrown if a folderItem with that name already exists.
* @throws InvalidNameException if the name has illegal characters. * @throws InvalidNameException if the name has illegal characters.
* @throws IOException if an IO error occurs. * @throws IOException if an IO error occurs.
*/ */
public TextDataItem createTextDataItem(String parentPath, String name, String fileID, public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
String contentType, String textData, String comment) String contentType, String textData, String comment, String user)
throws InvalidNameException, IOException; throws InvalidNameException, IOException;
/** /**
@ -344,23 +345,22 @@ public interface FileSystem {
*/ */
public static String normalizePath(String path) throws IllegalArgumentException { public static String normalizePath(String path) throws IllegalArgumentException {
if (!path.startsWith(SEPARATOR)) { if (!path.startsWith(SEPARATOR)) {
throw new IllegalArgumentException("Absolute path required"); throw new IllegalArgumentException("Absolute path required: " + path);
} }
String[] split = path.split(SEPARATOR); String[] split = path.split(SEPARATOR);
ArrayList<String> elements = new ArrayList<>(); ArrayList<String> elements = new ArrayList<>();
elements.add(SEPARATOR);
for (int i = 1; i < split.length; i++) { for (int i = 1; i < split.length; i++) {
String e = split[i]; String e = split[i];
if (e.length() == 0) { if (e.length() == 0) {
throw new IllegalArgumentException("Invalid path with empty element: " + path); throw new IllegalArgumentException("Invalid path with empty element: " + path);
} }
if ("..".equals(e)) { if ("..".equals(e)) {
try {
// remove last element // remove last element
elements.removeLast(); elements.removeLast();
} if (elements.size() == 0) {
catch (NoSuchElementException ex) {
throw new IllegalArgumentException("Invalid path: " + path); throw new IllegalArgumentException("Invalid path: " + path);
} }
} }
@ -369,6 +369,9 @@ public interface FileSystem {
continue; continue;
} }
else { else {
if (i < (split.length - 1)) {
e += SEPARATOR;
}
elements.add(e); elements.add(e);
} }
} }
@ -379,9 +382,11 @@ public interface FileSystem {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
for (String e : elements) { for (String e : elements) {
buf.append(SEPARATOR);
buf.append(e); buf.append(e);
} }
if (path.endsWith(SEPARATOR)) {
buf.append(SEPARATOR);
}
return buf.toString(); return buf.toString();
} }

View file

@ -556,7 +556,7 @@ public abstract class LocalFileSystem implements FileSystem {
@Override @Override
public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name, public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name,
String fileID, String contentType, String textData, String ignoredComment) String fileID, String contentType, String textData, String comment, String user)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
// comment is ignored // comment is ignored
@ -573,6 +573,12 @@ public abstract class LocalFileSystem implements FileSystem {
try { try {
ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData); linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData);
if (isVersioned) {
Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
linkFile.setVersionInfo(singleVersion);
}
linkFile.log("file created", getUserName()); linkFile.log("file created", getUserName());
} }
finally { finally {
@ -822,30 +828,40 @@ public abstract class LocalFileSystem implements FileSystem {
} }
/** /**
* Returns the full path for a specific folder or item * Returns the full path for a named folder or item within a parent folder
* @param parentPath full parent path * @param parentPath full parent path
* @param name child folder or item name * @param name child folder or item name
* @return pathname * @return pathname
*/ */
protected final static String getPath(String parentPath, String name) { public final static String getPath(String parentPath, String name) {
if (parentPath.length() == 1) { if (parentPath.length() == 1) {
return parentPath + name; return parentPath + name;
} }
return parentPath + SEPARATOR_CHAR + name; return parentPath + SEPARATOR_CHAR + name;
} }
protected final static String getParentPath(String path) { /**
* Returns the full parent path for a specific folder or item path
* @param path full path of folder or item
* @return parent path or null if "/" path was specified
*/
public final static String getParentPath(String path) {
int index = path.lastIndexOf(SEPARATOR_CHAR);
if (index == 0) {
if (path.length() == 1) { if (path.length() == 1) {
return null; return null;
} }
int index = path.lastIndexOf(SEPARATOR_CHAR);
if (index == 0) {
return SEPARATOR; return SEPARATOR;
} }
return path.substring(0, index); return path.substring(0, index);
} }
protected final static String getName(String path) { /**
* Returns the name for a specific folder or item path
* @param path full path of folder or item
* @return parent path or null if "/" path was specified
*/
public final static String getName(String path) {
if (path.length() == 1) { if (path.length() == 1) {
return path; return path;
} }

View file

@ -89,14 +89,14 @@ public abstract class LocalFolderItem implements FolderItem {
* @param useDataDir if true the getDataDir() method must return an appropriate * @param useDataDir if true the getDataDir() method must return an appropriate
* directory for data storage. * directory for data storage.
* @param create if true the data directory will be created * @param create if true the data directory will be created
* @throws IOException * @throws IOException if an IO error occurs
*/ */
LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir, LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir,
boolean create) throws IOException { boolean create) throws IOException {
this.fileSystem = fileSystem; this.fileSystem = fileSystem;
this.propertyFile = propertyFile; this.propertyFile = propertyFile;
this.isVersioned = fileSystem.isVersioned(); this.isVersioned = fileSystem.isVersioned();
this.useDataDir = useDataDir || isVersioned; this.useDataDir = useDataDir;
boolean success = false; boolean success = false;
try { try {

View file

@ -229,7 +229,7 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
@Override @Override
public TextDataItem createTextDataItem(String parentPath, String name, String fileID, public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
String contentType, String textData, String comment) String contentType, String textData, String comment, String ignoredUser)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment); repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment);
return (TextDataItem) getItem(parentPath, name); return (TextDataItem) getItem(parentPath, name);

View file

@ -729,6 +729,10 @@ public class DefaultProjectData implements ProjectData {
@Override @Override
public void refresh(boolean force) { public void refresh(boolean force) {
// FIXME: We ignore force. We are forcing full recursive refresh on non-visited folders
// only - seems inconsistent!!
// Underlying method fails if recursive and force is false.
// NOTE: Refresh really does nothing if force is false and folder already visited
try { try {
rootFolderData.refresh(true, true, projectDisposalMonitor); rootFolderData.refresh(true, true, projectDisposalMonitor);
} }
@ -1057,7 +1061,8 @@ public class DefaultProjectData implements ProjectData {
@Override @Override
public void folderCreated(final String parentPath, final String name) { public void folderCreated(final String parentPath, final String name) {
synchronized (fileSystem) { synchronized (fileSystem) {
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, true); boolean lazy = !rootFolderData.mustVisit(parentPath);
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, lazy);
if (folderData != null) { if (folderData != null) {
try { try {
folderData.folderChanged(name); folderData.folderChanged(name);
@ -1111,7 +1116,9 @@ public class DefaultProjectData implements ProjectData {
// ignore // ignore
} }
} }
folderData = rootFolderData.getFolderPathData(newParentPath, true);
boolean lazy = !rootFolderData.mustVisit(newParentPath);
folderData = rootFolderData.getFolderPathData(newParentPath, lazy);
if (folderData != null) { if (folderData != null) {
try { try {
folderData.folderChanged(folderName); folderData.folderChanged(folderName);
@ -1338,7 +1345,7 @@ public class DefaultProjectData implements ProjectData {
} }
} }
GhidraFolderData getRootFolderData() { RootGhidraFolderData getRootFolderData() {
return rootFolderData; return rootFolderData;
} }

View file

@ -165,6 +165,16 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
} }
} }
@Override
public boolean isFolderLink() {
try {
return getFileData().isFolderLink();
}
catch (IOException e) {
return false;
}
}
@Override @Override
public LinkFileInfo getLinkInfo() { public LinkFileInfo getLinkInfo() {
return isLink() ? this : null; return isLink() ? this : null;
@ -642,7 +652,7 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
catch (IOException e) { catch (IOException e) {
fileError(e); fileError(e);
} }
return new HashMap<>(); return Map.of();
} }
void fileChanged() { void fileChanged() {

View file

@ -81,7 +81,12 @@ public class GhidraFileData {
private GhidraFolderData parent; private GhidraFolderData parent;
private String name; private String name;
private String fileID; private String fileID;
private String contentType;
private String linkPath; private String linkPath;
private String absoluteLinkPath;
private boolean isLink;
private boolean isFolderLink;
private LocalFolderItem folderItem; private LocalFolderItem folderItem;
private FolderItem versionedFolderItem; private FolderItem versionedFolderItem;
@ -98,7 +103,10 @@ public class GhidraFileData {
// longer used. // longer used.
/** /**
* Construct a file instance with a specified name and a correpsonding parent folder * Construct a file instance with a specified name and a correpsonding parent folder.
* It is important that this object only be instantiated by the
* {@link GhidraFolderData} parent supplied and properly cached and tracked to ensure proper
* tracking and link registration.
* @param parent parent folder * @param parent parent folder
* @param name file name * @param name file name
* @throws IOException if an IO error occurs * @throws IOException if an IO error occurs
@ -118,7 +126,9 @@ public class GhidraFileData {
/** /**
* Construct a new file instance with a specified name and a corresponding parent folder using * Construct a new file instance with a specified name and a corresponding parent folder using
* up-to-date folder items. * up-to-date folder items. It is important that this object only be instantiated by the
* {@link GhidraFolderData} parent supplied and properly cached and tracked to ensure proper
* tracking and link registration.
* @param parent parent folder * @param parent parent folder
* @param name file name * @param name file name
* @param folderItem local folder item * @param folderItem local folder item
@ -138,10 +148,15 @@ public class GhidraFileData {
validateCheckout(); validateCheckout();
updateFileID(); updateFileID();
registerLinkFile();
} }
void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) { void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) {
linkPath = null;
unregisterLinkFile();
contentType = null;
icon = null; icon = null;
disabledIcon = null; disabledIcon = null;
@ -151,6 +166,8 @@ public class GhidraFileData {
validateCheckout(); validateCheckout();
boolean fileIDset = updateFileID(); boolean fileIDset = updateFileID();
registerLinkFile();
if (parent.visited()) { if (parent.visited()) {
// NOTE: we should maintain some cached data so we can determine if something really changed // NOTE: we should maintain some cached data so we can determine if something really changed
listener.domainFileStatusChanged(getDomainFile(), fileIDset); listener.domainFileStatusChanged(getDomainFile(), fileIDset);
@ -158,7 +175,13 @@ public class GhidraFileData {
} }
private boolean refresh() throws IOException { private boolean refresh() throws IOException {
linkPath = null;
unregisterLinkFile();
contentType = null;
icon = null;
disabledIcon = null;
String parentPath = parent.getPathname(); String parentPath = parent.getPathname();
if (folderItem == null) { if (folderItem == null) {
folderItem = fileSystem.getItem(parentPath, name); folderItem = fileSystem.getItem(parentPath, name);
@ -183,7 +206,54 @@ public class GhidraFileData {
if (folderItem == null && versionedFolderItem == null) { if (folderItem == null && versionedFolderItem == null) {
throw new FileNotFoundException(name + " not found"); throw new FileNotFoundException(name + " not found");
} }
return updateFileID();
boolean fileIDset = updateFileID();
registerLinkFile();
return fileIDset;
}
private void registerLinkFile() {
try {
ContentHandler<?> contentHandler = getContentHandler();
isLink = LinkHandler.class.isAssignableFrom(contentHandler.getClass());
if (!isLink) {
return;
}
isFolderLink =
FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType());
getLinkPath(true); // will cache linkPath and absoluteLinkPath
if (absoluteLinkPath == null) {
return;
}
// Avoid registering circular reference
if (isFolderLink && getPathname().startsWith(absoluteLinkPath)) {
return;
}
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
rootFolderData.registerInternalLinkPath(absoluteLinkPath);
}
catch (IOException e) {
// Too much noise if we report IOExceptions. If it happens to one file it could happen
// with many files.
return;
}
}
private void unregisterLinkFile() {
if (absoluteLinkPath != null) {
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
rootFolderData.unregisterInternalLinkPath(absoluteLinkPath);
}
linkPath = null;
absoluteLinkPath = null;
isLink = false;
isFolderLink = false;
} }
private boolean updateFileID() { private boolean updateFileID() {
@ -204,8 +274,6 @@ public class GhidraFileData {
if (mergeInProgress) { if (mergeInProgress) {
return; return;
} }
icon = null;
disabledIcon = null;
fileIDset |= refresh(); fileIDset |= refresh();
if (parent.visited()) { if (parent.visited()) {
// NOTE: we should maintain some cached data so we can determine if something really changed // NOTE: we should maintain some cached data so we can determine if something really changed
@ -267,6 +335,7 @@ public class GhidraFileData {
*/ */
void dispose() { void dispose() {
projectData.removeFromIndex(fileID); projectData.removeFromIndex(fileID);
unregisterLinkFile();
// NOTE: clearing the following can cause issues since there may be some residual // NOTE: clearing the following can cause issues since there may be some residual
// activity/use which will get a NPE // activity/use which will get a NPE
// parent = null; // parent = null;
@ -434,10 +503,6 @@ public class GhidraFileData {
} }
} }
boolean isFolderLink() {
return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType());
}
/** /**
* Returns content-type string for this file * Returns content-type string for this file
* @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT} * @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
@ -445,14 +510,22 @@ public class GhidraFileData {
*/ */
String getContentType() { String getContentType() {
synchronized (fileSystem) { synchronized (fileSystem) {
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION); if (contentType != null) {
// this can happen when we are trying to load a version file from return contentType;
// a server to which we are not connected
if (item == null) {
return ContentHandler.MISSING_CONTENT;
} }
String contentType = item.getContentType(); FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
return contentType != null ? contentType : ContentHandler.UNKNOWN_CONTENT; if (item == null) {
// This can happen when we are trying to load a version file from
// a server to which we are not connected
contentType = ContentHandler.MISSING_CONTENT;
}
else {
contentType = item.getContentType();
if (contentType == null) {
contentType = ContentHandler.UNKNOWN_CONTENT;
}
}
return contentType;
} }
} }
@ -1235,7 +1308,7 @@ public class GhidraFileData {
else if (folderItem instanceof TextDataItem textDataItem) { else if (folderItem instanceof TextDataItem textDataItem) {
versionedFileSystem.createTextDataItem(parentPath, name, versionedFileSystem.createTextDataItem(parentPath, name,
folderItem.getFileID(), folderItem.getContentType(), folderItem.getFileID(), folderItem.getContentType(),
textDataItem.getTextData(), comment); textDataItem.getTextData(), comment, user);
} }
else { else {
throw new IOException( throw new IOException(
@ -2237,17 +2310,26 @@ public class GhidraFileData {
* @return true if link file else false for a normal domain file * @return true if link file else false for a normal domain file
*/ */
boolean isLink() { boolean isLink() {
try { return isLink; // relies on refresh to initialize
return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
}
catch (IOException e) {
return false;
} }
/**
* Determine if this file is a link file which corresponds to a folder link.
* If this is a folder-link it should not be used to obtain a {@link DomainObject}.
* The link path or URL stored within the link-file may be read using {@link #getLinkPath(boolean)}.
* The content type (see {@link #getContentType()} of a folder-link will be
* {@link FolderLinkContentHandler}.
* @return true if link file else false for a normal domain file
*/
boolean isFolderLink() {
return isFolderLink; // relies on refresh to initialize
} }
/** /**
* If this is a {@link #isLink() link file} this method will return the link-path which * If this is a {@link #isLink() link file} this method will return the link-path which
* may be either an absolute or relative path within the the project or a Ghidra URL. * may be either an absolute or relative path within the the project or a Ghidra URL.
* Invoking with {@code resolve==true} will ensure that both {@code linkPath} and
* {@code absoluteLinkPath} get properly cached.
* *
* @param resolve if true relative paths will always be converted to an absolute path * @param resolve if true relative paths will always be converted to an absolute path
* @return associated link path or null if not a link file * @return associated link path or null if not a link file
@ -2275,12 +2357,19 @@ public class GhidraFileData {
return linkPath; return linkPath;
} }
String path = linkPath;
if (!GhidraURL.isGhidraURL(linkPath)) { if (!GhidraURL.isGhidraURL(linkPath)) {
path = getAbsolutePath(linkPath); if (absoluteLinkPath == null) {
try {
absoluteLinkPath = getAbsolutePath(linkPath);
}
catch (IllegalArgumentException e) {
return null;
}
}
return absoluteLinkPath;
} }
return path; return linkPath;
} }
private String getAbsolutePath(String path) throws IOException { private String getAbsolutePath(String path) throws IOException {
@ -2292,13 +2381,7 @@ public class GhidraFileData {
} }
absPath += path; absPath += path;
} }
try { return FileSystem.normalizePath(absPath);
absPath = FileSystem.normalizePath(absPath);
}
catch (IllegalArgumentException e) {
throw new IOException("Invalid link path: " + linkPath);
}
return absPath;
} }
/** /**
@ -2396,7 +2479,7 @@ public class GhidraFileData {
if (!StringUtils.isBlank(lp)) { if (!StringUtils.isBlank(lp)) {
newParent.getLocalFileSystem() newParent.getLocalFileSystem()
.createTextDataItem(pathname, targetName, .createTextDataItem(pathname, targetName,
FileIDFactory.createFileID(), contentType, lp, null); FileIDFactory.createFileID(), contentType, lp, null, null);
} }
else { else {
throw new IOException( throw new IOException(

View file

@ -64,7 +64,7 @@ class GhidraFolderData {
// folderList and fileList are only be used if visited is true // folderList and fileList are only be used if visited is true
private Set<String> folderList = new TreeSet<>(); private Set<String> folderList = new TreeSet<>();
private boolean visited; // true if full refresh was performed private boolean visited; // true if full refresh was performed and change notifications get sent
private Map<String, GhidraFileData> fileDataCache = new HashMap<>(); private Map<String, GhidraFileData> fileDataCache = new HashMap<>();
private Map<String, GhidraFolderData> folderDataCache = new HashMap<>(); private Map<String, GhidraFolderData> folderDataCache = new HashMap<>();
@ -122,6 +122,15 @@ class GhidraFolderData {
return visited; return visited;
} }
/**
* @return true if this folder must be visited when created to ensure that related change
* notifications are properly conveyed.
*/
boolean mustVisit() {
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
return rootFolderData.mustVisit(getPathname());
}
/** /**
* @return local file system * @return local file system
*/ */
@ -283,7 +292,7 @@ class GhidraFolderData {
parent.folderList.remove(oldName); parent.folderList.remove(oldName);
parent.folderList.add(newName); parent.folderList.add(newName);
// Must force refresh to ensure that all folder items are properly updted with new parent path // Must force refresh to ensure that all folder items are properly updated with new parent path
refresh(true, true, projectData.getProjectDisposalMonitor()); refresh(true, true, projectData.getProjectDisposalMonitor());
listener.domainFolderRenamed(newFolder, oldName); listener.domainFolderRenamed(newFolder, oldName);
@ -385,9 +394,7 @@ class GhidraFolderData {
* @param newFileName file name after rename * @param newFileName file name after rename
*/ */
void fileRenamed(String oldFileName, String newFileName) { void fileRenamed(String oldFileName, String newFileName) {
GhidraFileData fileData; GhidraFileData fileData = fileDataCache.remove(oldFileName);
synchronized (fileSystem) {
fileData = fileDataCache.remove(oldFileName);
if (fileData == null || this != fileData.getParent() || if (fileData == null || this != fileData.getParent() ||
!newFileName.equals(fileData.getName())) { !newFileName.equals(fileData.getName())) {
throw new AssertException(); throw new AssertException();
@ -397,7 +404,6 @@ class GhidraFolderData {
listener.domainFileRenamed(getDomainFile(newFileName), oldFileName); listener.domainFileRenamed(getDomainFile(newFileName), oldFileName);
} }
} }
}
/** /**
* Update file list/cache based upon change of parent for a file. * Update file list/cache based upon change of parent for a file.
@ -510,8 +516,7 @@ class GhidraFolderData {
* if this folder has been visited * if this folder has been visited
* @param folderName name of folder which was removed * @param folderName name of folder which was removed
*/ */
void folderRemoved(String folderName) { private void folderRemoved(String folderName) {
synchronized (fileSystem) {
GhidraFolderData folderData = folderDataCache.remove(folderName); GhidraFolderData folderData = folderDataCache.remove(folderName);
if (folderData != null) { if (folderData != null) {
folderData.dispose(); folderData.dispose();
@ -520,7 +525,6 @@ class GhidraFolderData {
listener.domainFolderRemoved(getDomainFolder(), folderName); listener.domainFolderRemoved(getDomainFolder(), folderName);
} }
} }
}
/** /**
* Disposes the cached data for this folder and all of its children recursively. * Disposes the cached data for this folder and all of its children recursively.
@ -561,6 +565,8 @@ class GhidraFolderData {
*/ */
private void refreshFolders(boolean recursive, TaskMonitor monitor) throws IOException { private void refreshFolders(boolean recursive, TaskMonitor monitor) throws IOException {
// FIXME: inconsistent use of forced-recursive refresh and cached folderList
String path = getPathname(); String path = getPathname();
HashSet<String> newSet = new HashSet<>(); HashSet<String> newSet = new HashSet<>();
@ -772,6 +778,10 @@ class GhidraFolderData {
void refresh(boolean recursive, boolean force, TaskMonitor monitor) throws IOException { void refresh(boolean recursive, boolean force, TaskMonitor monitor) throws IOException {
synchronized (fileSystem) { synchronized (fileSystem) {
if (recursive && !force) { if (recursive && !force) {
// FIXME: Why must this restriction be imposed. We need a lazy refresh that only refreshes
// those folders that have been visited or must be visited.
throw new IllegalArgumentException("force must be true when recursive"); throw new IllegalArgumentException("force must be true when recursive");
} }
if (monitor != null && monitor.isCancelled()) { if (monitor != null && monitor.isCancelled()) {
@ -780,6 +790,9 @@ class GhidraFolderData {
if (visited && !force) { if (visited && !force) {
return; return;
} }
visited = true;
try { try {
updateExistenceState(); updateExistenceState();
} }
@ -797,7 +810,8 @@ class GhidraFolderData {
throw new FileNotFoundException("Folder not found: " + getPathname()); throw new FileNotFoundException("Folder not found: " + getPathname());
} }
try { // FIXME: If forced we should be refreshing folder/file lists
refreshFiles(monitor); refreshFiles(monitor);
if (monitor != null && monitor.isCancelled()) { if (monitor != null && monitor.isCancelled()) {
@ -806,10 +820,6 @@ class GhidraFolderData {
refreshFolders(recursive, monitor); refreshFolders(recursive, monitor);
} }
finally {
visited = true;
}
}
} }
/** /**
@ -843,8 +853,11 @@ class GhidraFolderData {
try { try {
folderData = new GhidraFolderData(this, folderName); folderData = new GhidraFolderData(this, folderName);
folderDataCache.put(folderName, folderData); folderDataCache.put(folderName, folderData);
if (folderData.mustVisit()) {
folderData.refresh(false, true, TaskMonitor.DUMMY);
} }
catch (FileNotFoundException e) { }
catch (IOException e) {
// ignore // ignore
} }
} }
@ -1226,6 +1239,10 @@ class GhidraFolderData {
GhidraFolder newFolder = getDomainFolder(); GhidraFolder newFolder = getDomainFolder();
if (parent.visited || newParent.visited) { if (parent.visited || newParent.visited) {
// Must force refresh to ensure that all folder items are properly updated with new parent path
refresh(true, true, projectData.getProjectDisposalMonitor());
listener.domainFolderMoved(newFolder, oldParent); listener.domainFolderMoved(newFolder, oldParent);
} }
@ -1398,17 +1415,34 @@ class GhidraFolderData {
String linkFilename, LinkHandler<?> lh) throws IOException { String linkFilename, LinkHandler<?> lh) throws IOException {
synchronized (fileSystem) { synchronized (fileSystem) {
if (fileSystem.isReadOnly()) { if (fileSystem.isReadOnly()) {
throw new ReadOnlyException("copyAsLink permitted to writeable project only"); throw new ReadOnlyException("createLinkFile permitted to writeable project only");
} }
boolean referenceMyProject = (sourceProjectData == projectData);
boolean isFolderLink = (lh instanceof FolderLinkContentHandler);
if (!pathname.startsWith(FileSystem.SEPARATOR)) { if (!pathname.startsWith(FileSystem.SEPARATOR)) {
throw new IllegalArgumentException("invalid pathname specified"); throw new IllegalArgumentException(
"invalid absolute pathname specified: " + pathname);
} }
if (isFolderLink) {
// Force folder link path to end with "/" for GhidraURL case to disambiguate
if (!referenceMyProject && !pathname.endsWith(FileSystem.SEPARATOR)) {
pathname += FileSystem.SEPARATOR;
}
}
else if (pathname.endsWith(FileSystem.SEPARATOR) || pathname.endsWith("/.") ||
pathname.endsWith("/..")) {
throw new IllegalArgumentException("invalid file pathname specified: " + pathname);
}
pathname = FileSystem.normalizePath(pathname);
String linkPath; String linkPath;
if (sourceProjectData == projectData) { if (referenceMyProject) {
if (makeRelative) { if (makeRelative) {
linkPath = getRelativePath(pathname, getPathname()); linkPath = getRelativePath(pathname, getPathname(), isFolderLink);
} }
else { else {
linkPath = pathname; linkPath = pathname;
@ -1495,12 +1529,48 @@ class GhidraFolderData {
} }
} }
private static String getRelativePath(String referencedPathname, String linkParentPathname) { /**
Path referencedPath = Paths.get(referencedPathname); *
* @param normalizedReferencedPathname an absolute normalized folder/file reference path
* (see {@link FileSystem#normalizePath(String)}).
* @param linkParentPathname an absolute Ghidra folder pathname which will be the origin
* of the returned relative path and will be the folder where the lin-file is stored.
* @param isFolderRef true if {@code normalizedReferencedPathname} refers to a folder, else false
* @return relative path
* @throws IllegalArgumentException if referenced path cannot be relativized. This should not
* occur if absolute normalized path arguments are properly formed and are legal.
*/
static String getRelativePath(String normalizedReferencedPathname, String linkParentPathname,
boolean isFolderRef) throws IllegalArgumentException {
String finalRefElement = null;
if (!isFolderRef && !normalizedReferencedPathname.endsWith(FileSystem.SEPARATOR)) {
// Preserve last element name which may not be a folder name if not within root folder
int lastSepIx = normalizedReferencedPathname.lastIndexOf(FileSystem.SEPARATOR);
if (lastSepIx != 0) {
finalRefElement = normalizedReferencedPathname.substring(lastSepIx + 1);
normalizedReferencedPathname = normalizedReferencedPathname.substring(0, lastSepIx);
}
}
Path referencedPath = Paths.get(normalizedReferencedPathname);
Path linkParentPath = Paths.get(linkParentPathname); Path linkParentPath = Paths.get(linkParentPathname);
Path relativePath = linkParentPath.relativize(referencedPath); Path relativePath = linkParentPath.relativize(referencedPath);
String path = relativePath.toString(); String path = relativePath.toString();
if (referencedPathname.endsWith(FileSystem.SEPARATOR) &&
// Re-apply preserved finalRefElement to relative path
if (finalRefElement != null) {
if (!path.isBlank()) {
path += FileSystem.SEPARATOR;
}
path += finalRefElement;
}
if (path.isBlank()) {
return ".";
}
if (normalizedReferencedPathname.endsWith(FileSystem.SEPARATOR) &&
!path.endsWith(FileSystem.SEPARATOR)) { !path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR; path += FileSystem.SEPARATOR;
} }

View file

@ -103,7 +103,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
String linkFilename) throws IOException, InvalidNameException { String linkFilename) throws IOException, InvalidNameException {
fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(), fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(),
getContentType(), linkPath, null); getContentType(), linkPath, null, null);
} }
@Override @Override
@ -334,9 +334,20 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
} }
String path = linkPath; String path = linkPath;
boolean isRelative = false;
if (!GhidraURL.isGhidraURL(path)) { if (!GhidraURL.isGhidraURL(path)) {
if (!linkPath.startsWith(FileSystem.SEPARATOR)) { if (!linkPath.startsWith(FileSystem.SEPARATOR)) {
path = linkFile.getParent().getPathname(); isRelative = true;
DomainFolder parent;
if (linkFile instanceof LinkedDomainFile linkedFile) {
// Relative to real file's parent
parent = linkedFile.getRealFile().getParent();
}
else {
// Relative to link-file's parent
parent = linkFile.getParent();
}
path = parent.getPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) { if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR; path += FileSystem.SEPARATOR;
} }
@ -346,7 +357,11 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
return FileSystem.normalizePath(path); return FileSystem.normalizePath(path);
} }
catch (IllegalArgumentException e) { catch (IllegalArgumentException e) {
throw new IOException("Invalid link path: " + linkPath); String hint = "";
if (isRelative && linkFile instanceof LinkedDomainFile) {
hint = " (relative to real link-file)";
}
throw new IOException("Invalid link path: " + path + hint);
} }
} }
return path; return path;
@ -370,11 +385,16 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
/** /**
* Add real internal folder path for specified folder or folder-link and check for * Add real internal folder path for specified folder or folder-link and check for
* circular conflict. * circular conflict.
*
* NOTE: This is only useful in detecting a self-referencing
* path and not those that involve multiple independent linked-folders that could
* form circular paths.
*
* @param pathSet real path accumulator * @param pathSet real path accumulator
* @param linkPath internal linkPath * @param linkPath internal linkPath
* @return true if no path conflict detected, false if path conflict is detected * @return true if no path conflict detected, false if path conflict is detected
*/ */
private static boolean addLinkPathPath(Set<String> pathSet, String linkPath) { private static boolean addLinkPath(Set<String> pathSet, String linkPath) {
// Must ensure that all paths end with '/' separator - even if path is endpoint // Must ensure that all paths end with '/' separator - even if path is endpoint
if (!linkPath.endsWith(FileSystem.SEPARATOR)) { if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
linkPath += FileSystem.SEPARATOR; linkPath += FileSystem.SEPARATOR;
@ -418,7 +438,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
if (parent instanceof LinkedDomainFolder lf) { if (parent instanceof LinkedDomainFolder lf) {
try { try {
projectData = lf.getLinkedProjectData(); projectData = lf.getLinkedProjectData();
addLinkPathPath(linkPathsVisited, lf.getLinkedPathname()); addLinkPath(linkPathsVisited, lf.getLinkedPathname());
} }
catch (IOException e) { catch (IOException e) {
throw new RuntimeException("Unexpected", e); throw new RuntimeException("Unexpected", e);
@ -426,7 +446,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
} }
else { else {
projectData = parent.getProjectData(); projectData = parent.getProjectData();
addLinkPathPath(linkPathsVisited, file.getPathname()); addLinkPath(linkPathsVisited, file.getPathname());
} }
String contentType = file.getContentType(); String contentType = file.getContentType();
@ -466,19 +486,22 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
return nextLinkFile; return nextLinkFile;
} }
if (!addLinkPathPath(linkPathsVisited, linkPath)) {
errorConsumer.accept("Link has a circular reference");
break; // broken and can't continue
}
DomainFile linkedFile = null; DomainFile linkedFile = null;
if (!linkPath.endsWith(FileSystem.SEPARATOR)) { if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
linkedFile = projectData.getFile(linkPath); linkedFile = projectData.getFile(linkPath);
} }
DomainFolder linkedFolder = null;
if (isFolderLink) { if (isFolderLink) {
// Check for folder existence at linkPath linkedFolder = getNonLinkedFolder(projectData, linkPath);
if (getNonLinkedFolder(projectData, linkPath) != null) { }
if (linkedFolder == null && !addLinkPath(linkPathsVisited, linkPath)) {
errorConsumer.accept("Link has a circular reference");
break; // broken and can't continue
}
if (isFolderLink && linkedFolder != null) {
// Check for folder-link that conflicts with folder found // Check for folder-link that conflicts with folder found
if (linkedFile != null) { if (linkedFile != null) {
LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo(); LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo();
@ -492,7 +515,6 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
statusConsumer.accept(LinkStatus.INTERNAL); statusConsumer.accept(LinkStatus.INTERNAL);
return nextLinkFile; return nextLinkFile;
} }
}
if (linkedFile == null) { if (linkedFile == null) {
String acceptableType = isFolderLink ? "folder" : "file"; String acceptableType = isFolderLink ? "folder" : "file";

View file

@ -43,21 +43,21 @@ class LinkedGhidraFile implements LinkedDomainFile {
private final LinkedGhidraSubFolder parent; private final LinkedGhidraSubFolder parent;
private final String fileName; private final String fileName;
private final DomainFile realDomainFile;
private final LinkFileInfo linkInfo;
LinkedGhidraFile(LinkedGhidraSubFolder parent, String fileName) { LinkedGhidraFile(LinkedGhidraSubFolder parent, DomainFile realDomainFile) {
this.parent = parent; this.parent = parent;
this.fileName = fileName; this.fileName = realDomainFile.getName();
this.realDomainFile = realDomainFile;
this.linkInfo = realDomainFile.isLink() ? new LinkedFileLinkInfo() : null;
} }
@Override @Override
public DomainFile getLinkedFile() throws IOException { public DomainFile getRealFile() throws IOException {
return parent.getLinkedFile(fileName); return parent.getLinkedFile(fileName);
} }
private DomainFile getLinkedFileNoError() {
return parent.getLinkedFileNoError(fileName);
}
@Override @Override
public DomainFolder getParent() { public DomainFolder getParent() {
return parent; return parent;
@ -94,18 +94,18 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public boolean exists() { public boolean exists() {
return getLinkedFileNoError() != null; DomainFile df = parent.getLinkedFileNoError(fileName);
return df != null && df.exists();
} }
@Override @Override
public String getFileID() { public String getFileID() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getFileID();
return df != null ? df.getFileID() : null;
} }
@Override @Override
public DomainFile setName(String newName) throws InvalidNameException, IOException { public DomainFile setName(String newName) throws InvalidNameException, IOException {
String name = getLinkedFile().setName(newName).getName(); String name = getRealFile().setName(newName).getName();
return parent.getFile(name); return parent.getFile(name);
} }
@ -156,46 +156,40 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public String getContentType() { public String getContentType() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getContentType();
return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT;
} }
@Override @Override
public Class<? extends DomainObject> getDomainObjectClass() { public Class<? extends DomainObject> getDomainObjectClass() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getDomainObjectClass();
return df != null ? df.getDomainObjectClass() : DomainObject.class;
} }
@Override @Override
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException { public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
return getLinkedFile().getChangesByOthersSinceCheckout(); return getRealFile().getChangesByOthersSinceCheckout();
} }
@Override @Override
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException { TaskMonitor monitor) throws VersionException, IOException, CancelledException {
return getLinkedFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor); return getRealFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
} }
@Override @Override
public DomainObject getOpenedDomainObject(Object consumer) { public DomainObject getOpenedDomainObject(Object consumer) {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getOpenedDomainObject(consumer);
if (df != null) {
return df.getOpenedDomainObject(consumer);
}
return null;
} }
@Override @Override
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor) public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException { throws VersionException, IOException, CancelledException {
return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor); return getRealFile().getReadOnlyDomainObject(consumer, version, monitor);
} }
@Override @Override
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor) public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException { throws VersionException, IOException, CancelledException {
return getLinkedFile().getImmutableDomainObject(consumer, version, monitor); return getRealFile().getImmutableDomainObject(consumer, version, monitor);
} }
@Override @Override
@ -226,191 +220,174 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public long getLastModifiedTime() { public long getLastModifiedTime() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getLastModifiedTime();
return df != null ? df.getLastModifiedTime() : 0;
} }
@Override @Override
public Icon getIcon(boolean disabled) { public Icon getIcon(boolean disabled) {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getIcon(disabled);
return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON;
} }
@Override @Override
public boolean isCheckedOut() { public boolean isCheckedOut() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isCheckedOut();
return df != null ? df.isCheckedOut() : false;
} }
@Override @Override
public boolean isCheckedOutExclusive() { public boolean isCheckedOutExclusive() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isCheckedOutExclusive();
return df != null ? df.isCheckedOutExclusive() : false;
} }
@Override @Override
public boolean modifiedSinceCheckout() { public boolean modifiedSinceCheckout() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.modifiedSinceCheckout();
return df != null ? df.modifiedSinceCheckout() : false;
} }
@Override @Override
public boolean canCheckout() { public boolean canCheckout() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.canCheckout();
return df != null ? df.canCheckout() : false;
} }
@Override @Override
public boolean canCheckin() { public boolean canCheckin() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.canCheckin();
return df != null ? df.canCheckin() : false;
} }
@Override @Override
public boolean canMerge() { public boolean canMerge() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.canMerge();
return df != null ? df.canMerge() : false;
} }
@Override @Override
public boolean canAddToRepository() { public boolean canAddToRepository() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.canAddToRepository();
return df != null ? df.canAddToRepository() : false;
} }
@Override @Override
public void setReadOnly(boolean state) throws IOException { public void setReadOnly(boolean state) throws IOException {
getLinkedFile().setReadOnly(state); getRealFile().setReadOnly(state);
} }
@Override @Override
public boolean isReadOnly() { public boolean isReadOnly() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isReadOnly();
// read-only state not reflected by icon
return df != null ? df.isReadOnly() : true;
} }
@Override @Override
public boolean isVersioned() { public boolean isVersioned() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isVersioned();
return df != null ? df.isVersioned() : false;
} }
@Override @Override
public boolean isHijacked() { public boolean isHijacked() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isHijacked();
return df != null ? df.isHijacked() : false;
} }
@Override @Override
public int getLatestVersion() { public int getLatestVersion() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getLatestVersion();
return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION;
} }
@Override @Override
public boolean isLatestVersion() { public boolean isLatestVersion() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isLatestVersion();
return df != null ? df.isLatestVersion() : true;
} }
@Override @Override
public int getVersion() { public int getVersion() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getVersion();
return df != null ? df.getVersion() : DomainFile.DEFAULT_VERSION;
} }
@Override @Override
public Version[] getVersionHistory() throws IOException { public Version[] getVersionHistory() throws IOException {
DomainFile df = getLinkedFile(); DomainFile df = getRealFile();
return df != null ? df.getVersionHistory() : new Version[0]; return df != null ? df.getVersionHistory() : new Version[0];
} }
@Override @Override
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor) public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
getLinkedFile().addToVersionControl(comment, keepCheckedOut, monitor); getRealFile().addToVersionControl(comment, keepCheckedOut, monitor);
} }
@Override @Override
public boolean checkout(boolean exclusive, TaskMonitor monitor) public boolean checkout(boolean exclusive, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
return getLinkedFile().checkout(exclusive, monitor); return getRealFile().checkout(exclusive, monitor);
} }
@Override @Override
public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor) public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor)
throws IOException, VersionException, CancelledException { throws IOException, VersionException, CancelledException {
getLinkedFile().checkin(checkinHandler, monitor); getRealFile().checkin(checkinHandler, monitor);
} }
@Override @Override
public void merge(boolean okToUpgrade, TaskMonitor monitor) public void merge(boolean okToUpgrade, TaskMonitor monitor)
throws IOException, VersionException, CancelledException { throws IOException, VersionException, CancelledException {
getLinkedFile().merge(okToUpgrade, monitor); getRealFile().merge(okToUpgrade, monitor);
} }
@Override @Override
public void undoCheckout(boolean keep) throws IOException { public void undoCheckout(boolean keep) throws IOException {
getLinkedFile().undoCheckout(keep); getRealFile().undoCheckout(keep);
} }
@Override @Override
public void undoCheckout(boolean keep, boolean force) throws IOException { public void undoCheckout(boolean keep, boolean force) throws IOException {
getLinkedFile().undoCheckout(keep, force); getRealFile().undoCheckout(keep, force);
} }
@Override @Override
public void terminateCheckout(long checkoutId) throws IOException { public void terminateCheckout(long checkoutId) throws IOException {
getLinkedFile().terminateCheckout(checkoutId); getRealFile().terminateCheckout(checkoutId);
} }
@Override @Override
public ItemCheckoutStatus[] getCheckouts() throws IOException { public ItemCheckoutStatus[] getCheckouts() throws IOException {
return getLinkedFile().getCheckouts(); return getRealFile().getCheckouts();
} }
@Override @Override
public ItemCheckoutStatus getCheckoutStatus() throws IOException { public ItemCheckoutStatus getCheckoutStatus() throws IOException {
return getLinkedFile().getCheckoutStatus(); return getRealFile().getCheckoutStatus();
} }
@Override @Override
public void delete() throws IOException { public void delete() throws IOException {
getLinkedFile().delete(); getRealFile().delete();
} }
@Override @Override
public void delete(int version) throws IOException { public void delete(int version) throws IOException {
getLinkedFile().delete(version); getRealFile().delete(version);
} }
@Override @Override
public DomainFile moveTo(DomainFolder newParent) throws IOException { public DomainFile moveTo(DomainFolder newParent) throws IOException {
return getLinkedFile().moveTo(newParent); return getRealFile().moveTo(newParent);
} }
@Override @Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
return getLinkedFile().copyTo(newParent, monitor); return getRealFile().copyTo(newParent, monitor);
} }
@Override @Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
return getLinkedFile().copyVersionTo(version, destFolder, monitor); return getRealFile().copyVersionTo(version, destFolder, monitor);
} }
@Override @Override
public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
return getLinkedFile().copyToAsLink(newParent, relative); return getRealFile().copyToAsLink(newParent, relative);
} }
@Override @Override
public boolean isLinkingSupported() { public boolean isLinkingSupported() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isLinkingSupported();
return df != null ? df.isLinkingSupported() : false;
} }
@Override @Override
@ -420,8 +397,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public boolean isChanged() { public boolean isChanged() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.isChanged();
return df != null ? df.isChanged() : false;
} }
@Override @Override
@ -436,31 +412,57 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException { public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
getLinkedFile().packFile(file, monitor); getRealFile().packFile(file, monitor);
} }
@Override @Override
public Map<String, String> getMetadata() { public Map<String, String> getMetadata() {
DomainFile df = getLinkedFileNoError(); return realDomainFile.getMetadata();
return df != null ? df.getMetadata() : Map.of();
} }
@Override @Override
public long length() throws IOException { public long length() throws IOException {
DomainFile df = getLinkedFileNoError(); return realDomainFile.length();
return df != null ? df.length() : 0;
} }
@Override @Override
public boolean isLink() { public boolean isLink() {
DomainFile df = getLinkedFileNoError(); return linkInfo != null;
return df != null ? df.isLink() : false;
} }
@Override @Override
public LinkFileInfo getLinkInfo() { public LinkFileInfo getLinkInfo() {
DomainFile df = getLinkedFileNoError(); return linkInfo;
return df != null ? df.getLinkInfo() : null; }
private class LinkedFileLinkInfo implements LinkFileInfo {
@Override
public DomainFile getFile() {
return LinkedGhidraFile.this;
}
@Override
public LinkedGhidraFolder getLinkedFolder() {
try {
return FolderLinkContentHandler.getLinkedFolder(LinkedGhidraFile.this);
}
catch (IOException e) {
// Ignore
}
return null;
}
@Override
public String getLinkPath() {
return realDomainFile.getLinkInfo().getLinkPath();
}
@Override
public String getAbsoluteLinkPath() throws IOException {
return realDomainFile.getLinkInfo().getAbsoluteLinkPath();
}
} }
@Override @Override
@ -470,12 +472,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override @Override
public String toString() { public String toString() {
String str = parent.toString(); return getPathname() + "->" + realDomainFile.getPathname();
if (!str.endsWith("/")) {
str += "/";
}
str += getName();
return str;
} }
} }

View file

@ -57,7 +57,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* since an error will occur if there is no active project at the time the link is followed. * since an error will occur if there is no active project at the time the link is followed.
* *
* @param folderLinkFile link-file which corresponds to a linked-folder * @param folderLinkFile link-file which corresponds to a linked-folder
* (see {@link LinkFileInfo#isFolderLink()}). * (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
* of this linked-folder.
* @param linkedFolderUrl linked folder URL * @param linkedFolderUrl linked folder URL
*/ */
LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) { LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
@ -87,7 +88,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* {@link #getProjectData() project data} instance. * {@link #getProjectData() project data} instance.
* *
* @param folderLinkFile link-file which corresponds to a linked-folder * @param folderLinkFile link-file which corresponds to a linked-folder
* (see {@link LinkFileInfo#isFolderLink()}). * (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
* of this linked-folder.
* @param linkedFolder locally-linked folder within same project * @param linkedFolder locally-linked folder within same project
*/ */
LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) { LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
@ -114,7 +116,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
if (!(obj instanceof LinkedGhidraFolder other)) { if (!(obj instanceof LinkedGhidraFolder other)) {
return false; return false;
} }
return linkedPathname.equals(other.linkedPathname) && return getPathname().equals(other.getPathname()) &&
folderLinkFile.equals(other.folderLinkFile); folderLinkFile.equals(other.folderLinkFile);
} }
@ -238,9 +240,9 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
@Override @Override
public String toString() { public String toString() {
if (linkedFolder != null) { if (linkedFolder != null) {
return "->" + getLinkedPathname(); return getPathname() + "->" + getLinkedPathname();
} }
return "->" + linkedFolderUrl.toString(); return getPathname() + "->" + linkedFolderUrl.toString();
} }
@Override @Override

View file

@ -274,7 +274,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
DomainFile[] files = linkedFolder.getFiles(); DomainFile[] files = linkedFolder.getFiles();
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length]; LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
for (int i = 0; i < files.length; i++) { for (int i = 0; i < files.length; i++) {
linkedSubFolders[i] = new LinkedGhidraFile(this, files[i].getName()); linkedSubFolders[i] = new LinkedGhidraFile(this, files[i]);
} }
return linkedSubFolders; return linkedSubFolders;
} }
@ -286,6 +286,10 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
/** /**
* Get the true file within this linked folder. * Get the true file within this linked folder.
* <P>
* NOTE: The returned file is the "real" file and would be the have the equivalence:
* {@code folder.getLinkedFileNoError("X") == folder.getFile("X").getRealFile() }.
*
* @param name file name * @param name file name
* @return file or null if not found or error occurs * @return file or null if not found or error occurs
*/ */
@ -300,6 +304,16 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return null; return null;
} }
/**
* Get the true file within this linked folder.
* <P>
* NOTE: The returned file is the "real" file and would be the have the equivalence:
* {@code folder.getLinkedFile("X") == folder.getFile("X").getRealFile() }.
*
* @param name file name
* @return file or null if not found or error occurs
* @throws IOException if an IO error ocurs such as FileNotFoundException
*/
DomainFile getLinkedFile(String name) throws IOException { DomainFile getLinkedFile(String name) throws IOException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
DomainFile df = linkedFolder.getFile(name); DomainFile df = linkedFolder.getFile(name);
@ -311,8 +325,8 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override @Override
public DomainFile getFile(String name) { public DomainFile getFile(String name) {
DomainFile f = getLinkedFileNoError(name); DomainFile file = getLinkedFileNoError(name);
return f != null ? new LinkedGhidraFile(this, name) : null; return file != null ? new LinkedGhidraFile(this, file) : null;
} }
@Override @Override
@ -333,36 +347,40 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor) public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException { throws InvalidNameException, IOException, CancelledException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
return linkedFolder.createFile(name, obj, monitor); DomainFile file = linkedFolder.createFile(name, obj, monitor);
return getFile(file.getName());
} }
@Override @Override
public DomainFile createFile(String name, File packFile, TaskMonitor monitor) public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException { throws InvalidNameException, IOException, CancelledException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
return linkedFolder.createFile(name, packFile, monitor); DomainFile file = linkedFolder.createFile(name, packFile, monitor);
return getFile(file.getName());
} }
@Override @Override
public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
boolean makeRelative, String linkFilename, LinkHandler<?> lh) throws IOException { boolean makeRelative, String linkFilename, LinkHandler<?> lh) throws IOException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename, DomainFile file = linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative,
lh); linkFilename, lh);
return getFile(file.getName());
} }
@Override @Override
public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh) public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh)
throws IOException { throws IOException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh); DomainFile file = linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
return getFile(file.getName());
} }
@Override @Override
public DomainFolder createFolder(String name) throws InvalidNameException, IOException { public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
DomainFolder linkedFolder = getRealFolder(); DomainFolder linkedFolder = getRealFolder();
DomainFolder child = linkedFolder.createFolder(name); DomainFolder folder = linkedFolder.createFolder(name);
return new LinkedGhidraSubFolder(parent, child.getName()); return getFolder(folder.getName());
} }
@Override @Override

View file

@ -15,11 +15,26 @@
*/ */
package ghidra.framework.data; package ghidra.framework.data;
import java.io.IOException;
import java.util.HashMap;
import ghidra.framework.model.DomainFolderChangeListener; import ghidra.framework.model.DomainFolderChangeListener;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.util.task.TaskMonitor;
public class RootGhidraFolderData extends GhidraFolderData { public class RootGhidraFolderData extends GhidraFolderData {
// Folder path reference counts, associated with discovered file-links and folder-links,
// are tracked to ensure that such folders are visited immediately or upon their
// creation to ensure that the folder change listener is properly notified of all changes
// related to the folder paths contained within this map.
private HashMap<String, Integer> folderReferenceCounts = new HashMap<>();
/**
* Constructor for project data root folder.
* @param projectData project data
* @param listener folder change listener
*/
RootGhidraFolderData(DefaultProjectData projectData, DomainFolderChangeListener listener) { RootGhidraFolderData(DefaultProjectData projectData, DomainFolderChangeListener listener) {
super(projectData, listener); super(projectData, listener);
} }
@ -50,4 +65,97 @@ public class RootGhidraFolderData extends GhidraFolderData {
return true; return true;
} }
/**
* Determine if the specified folder path must be visited due to
* possible link references to the folder or one of its children.
* @param folderPathname folder pathname (not ending with '/')
* @return true if folder should be visited to ensure that changes are properly tracked
* with proper change notifications sent.
*/
public boolean mustVisit(String folderPathname) {
return folderReferenceCounts.containsKey(folderPathname);
}
/**
* Register internal file/folder-link to ensure we do not ignore change events which affect
* the referenced file/folder.
* @param absoluteLinkPath absolute internal path referenced by a link-file
*/
void registerInternalLinkPath(String absoluteLinkPath) {
if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
throw new IllegalArgumentException();
}
// Register path elements upto parent of absoluteLinkPath
String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
int folderElementCount = pathSplit.length - 1;
// Start at 1 since element 0 corresponds to root and will be empty
GhidraFolderData folderData = this;
StringBuilder pathBuilder = new StringBuilder();
for (int i = 1; i < folderElementCount; i++) {
String folderName = pathSplit[i];
if (folderName.length() == 0) {
// ignore blank names
continue;
}
if (folderData != null) {
folderData = folderData.getFolderData(folderName, false);
if (folderData != null && !folderData.visited()) {
try {
folderData.refresh(false, true, TaskMonitor.DUMMY);
}
catch (IOException e) {
// ignore - things may get out-of-sync
folderData = null;
}
}
}
// Increment folder reference count for all folders leading up to referenced folder
pathBuilder.append(FileSystem.SEPARATOR);
pathBuilder.append(folderName);
folderReferenceCounts.compute(pathBuilder.toString(),
(path, count) -> (count == null) ? 1 : ++count);
}
}
/**
* Unregister internal file/folder-link to ensure we do not ignore change events which affect
* the referenced file/folder.
* @param absoluteLinkPath absolute internal path referenced by a link-file
*/
void unregisterInternalLinkPath(String absoluteLinkPath) {
if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
throw new IllegalArgumentException();
}
// Register path elements upto parent of absoluteLinkPath
String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
int folderElementCount = pathSplit.length - 1;
// Start at 1 since element 0 corresponds to root and will be empty
StringBuilder pathBuilder = new StringBuilder();
for (int i = 1; i < folderElementCount; i++) {
String folderName = pathSplit[i];
if (folderName.length() == 0) {
// ignore blank names
continue;
}
// Increment folder reference count for all folders leading up to referenced folder
pathBuilder.append(FileSystem.SEPARATOR);
pathBuilder.append(folderName);
String path = pathBuilder.toString();
Integer count = folderReferenceCounts.get(path);
if (count != null) {
if (count == 1) {
folderReferenceCounts.remove(path);
}
else {
folderReferenceCounts.put(path, count - 1);
}
}
}
}
} }

View file

@ -54,6 +54,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.preferences.Preferences; import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User; import ghidra.framework.remote.User;
import ghidra.framework.store.FileSystem;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.filechooser.GhidraFileChooserModel; import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter; import ghidra.util.filechooser.GhidraFileFilter;
@ -137,6 +138,7 @@ public class FrontEndPlugin extends Plugin
private ProjectDataRenameAction renameAction; private ProjectDataRenameAction renameAction;
private ProjectDataOpenDefaultToolAction openAction; private ProjectDataOpenDefaultToolAction openAction;
private ProjectDataFollowLinkAction followLinkAction; private ProjectDataFollowLinkAction followLinkAction;
private ProjectDataSelectRealFileOrFolderAction selectRealFileOrFolderAction;
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction; private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
private ProjectDataCollapseAction<FrontEndProjectTreeContext> collapseAction; private ProjectDataCollapseAction<FrontEndProjectTreeContext> collapseAction;
private ProjectDataSelectAction selectAction; private ProjectDataSelectAction selectAction;
@ -221,6 +223,7 @@ public class FrontEndPlugin extends Plugin
// Top of popup menu actions - no group // Top of popup menu actions - no group
openAction = new ProjectDataOpenDefaultToolAction(owner, null); openAction = new ProjectDataOpenDefaultToolAction(owner, null);
followLinkAction = new ProjectDataFollowLinkAction(this, null); followLinkAction = new ProjectDataFollowLinkAction(this, null);
selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
String groupName = "Cut/copy/paste/new1"; String groupName = "Cut/copy/paste/new1";
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName); newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
@ -258,6 +261,7 @@ public class FrontEndPlugin extends Plugin
tool.addAction(deleteAction); tool.addAction(deleteAction);
tool.addAction(openAction); tool.addAction(openAction);
tool.addAction(followLinkAction); tool.addAction(followLinkAction);
tool.addAction(selectRealFileOrFolderAction);
tool.addAction(renameAction); tool.addAction(renameAction);
tool.addAction(expandAction); tool.addAction(expandAction);
tool.addAction(collapseAction); tool.addAction(collapseAction);
@ -1117,11 +1121,17 @@ public class FrontEndPlugin extends Plugin
showInViewedProject(LinkHandler.getLinkURL(lastLink), true); showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
} }
else if (!dataTreePanel.isShowing()) { else if (!dataTreePanel.isShowing()) {
// Filter table on absolute link path
String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile); String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
if (linkPath.startsWith(FileSystem.SEPARATOR) && linkPath.length() > 1) {
// Filter table on absolute internal link path
if (linkPath.endsWith(FileSystem.SEPARATOR)) {
// Remove trailing '/' from path to ensure we match
linkPath = linkPath.substring(0, linkPath.length() - 1);
}
dataTablePanel.setFilter(linkPath); dataTablePanel.setFilter(linkPath);
} }
} }
}
catch (IOException e) { catch (IOException e) {
Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage()); Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage());
} }

View file

@ -0,0 +1,103 @@
/* ###
* 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.main;
import java.io.IOException;
import docking.action.MenuData;
import ghidra.framework.main.datatable.FrontendProjectTreeAction;
import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.main.datatree.DataTree;
import ghidra.framework.model.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
public class ProjectDataSelectRealFileOrFolderAction extends FrontendProjectTreeAction {
private FrontEndPlugin plugin;
public ProjectDataSelectRealFileOrFolderAction(FrontEndPlugin plugin, String group) {
super("Select Real File or Folder", plugin.getName());
this.plugin = plugin;
setPopupMenuData(new MenuData(new String[] { "Select Real File" }, group));
setHelpLocation(new HelpLocation("FrontEndPlugin", "Select_Real_File_or_Folder"));
}
@Override
protected void actionPerformed(ProjectDataContext context) {
boolean isFolder = false;
String pathname;
try {
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
DomainFolder folder = context.getSelectedFolders().get(0);
if (!(folder instanceof LinkedDomainFolder linkedFolder)) {
return;
}
isFolder = true;
pathname = linkedFolder.getRealFolder().getPathname();
}
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0);
if (!(file instanceof LinkedDomainFile linkedFile)) {
return;
}
isFolder = false;
pathname = linkedFile.getRealFile().getPathname();
}
else {
return;
}
// Path is local to its project data tree
plugin.showInProjectTree(context.getProjectData(), pathname, isFolder);
}
catch (IOException e) {
Msg.showError(this, null, "Linked Content Error",
"Failed to resolve linked " + (isFolder ? "folder" : "file"), e);
return;
}
}
@Override
protected boolean isEnabledForContext(ProjectDataContext context) {
boolean enabled = false;
String contentType = "Content";
if (context.getComponent() instanceof DataTree) {
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
DomainFolder folder = context.getSelectedFolders().get(0);
if (folder instanceof LinkedDomainFolder) {
contentType = "Folder";
enabled = true;
}
}
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0);
if (file instanceof LinkedDomainFile) {
contentType = "File";
enabled = true;
}
}
}
if (enabled) {
setPopupMenuData(new MenuData(new String[] { "Select Real " + contentType },
getPopupMenuData().getMenuGroup()));
}
return enabled;
}
}

View file

@ -27,6 +27,9 @@ import docking.widgets.tree.GTreeNode;
import ghidra.framework.data.LinkHandler; import ghidra.framework.data.LinkHandler;
import ghidra.framework.main.datatree.DataTreeNode.NodeType; import ghidra.framework.main.datatree.DataTreeNode.NodeType;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.Msg;
import ghidra.util.Swing;
/** /**
* Class to handle changes when a domain folder changes; updates the * Class to handle changes when a domain folder changes; updates the
@ -49,6 +52,10 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only
// The refreshedTrackingSet is used to track recursive path refreshes to avoid infinite
// recursion. See updateLinkedContent and LinkedTreeNode.refreshLinks methods.
private HashSet<String> refreshedTrackingSet;
ChangeManager(ProjectDataTreePanel treePanel) { ChangeManager(ProjectDataTreePanel treePanel) {
this.treePanel = treePanel; this.treePanel = treePanel;
projectData = treePanel.getProjectData(); projectData = treePanel.getProjectData();
@ -75,11 +82,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFileAdded(DomainFile file) { public void domainFileAdded(DomainFile file) {
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink(); boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
String fileName = file.getName(); String fileName = file.getName();
DomainFolder parentFolder = file.getParent(); DomainFolder parentFolder = file.getParent();
updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink), updateLinkedContent(parentFolder.getPathname(), p -> addFileNode(p, fileName, isFolderLink),
ltn -> ltn.refreshLinks(fileName)); ltn -> ltn.refreshLinks(fileName));
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true); DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
if (folderNode != null && folderNode.isLoaded()) { if (folderNode != null && folderNode.isLoaded()) {
addFileNode(folderNode, fileName, isFolderLink); addFileNode(folderNode, fileName, isFolderLink);
@ -88,7 +97,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFileRemoved(DomainFolder parent, String name, String fileID) { public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
DomainFolderNode folderNode = findDomainFolderNode(parent, true); DomainFolderNode folderNode = findDomainFolderNode(parent, true);
if (folderNode != null) { if (folderNode != null) {
updateChildren(folderNode); updateChildren(folderNode);
@ -97,14 +108,16 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFileRenamed(DomainFile file, String oldName) { public void domainFileRenamed(DomainFile file, String oldName) {
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink(); boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
updateLinkedContent(file.getParent(), p -> { updateLinkedContent(file.getParent().getPathname(), p -> {
updateChildren(p); updateChildren(p);
addFileNode(p, file.getName(), isFolderLink); addFileNode(p, file.getName(), isFolderLink);
}, ltn -> { }, ltn -> {
ltn.refreshLinks(oldName); ltn.refreshLinks(oldName);
ltn.refreshLinks(file.getName()); ltn.refreshLinks(file.getName());
}); });
DomainFolder parent = file.getParent(); DomainFolder parent = file.getParent();
skipLinkUpdate = true; skipLinkUpdate = true;
try { try {
@ -124,10 +137,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFileStatusChanged(DomainFile file, boolean fileIDset) { public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
LinkFileInfo linkInfo = file.getLinkInfo();
boolean isFolderLink = linkInfo != null && linkInfo.isFolderLink();
DomainFolder parentFolder = file.getParent(); DomainFolder parentFolder = file.getParent();
updateLinkedContent(parentFolder, fn -> { updateLinkedContent(parentFolder.getPathname(), fn -> {
/* No folder update required */ // Refresh any linked folder content containing file
if (fn.isLoaded()) {
NodeType type = isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE;
DomainFileNode fileNode = (DomainFileNode) fn.getChild(file.getName(), type);
if (fileNode != null) {
fileNode.refresh();
}
}
}, ltn -> ltn.refreshLinks(file.getName())); }, ltn -> ltn.refreshLinks(file.getName()));
DomainFileNode fileNode = findDomainFileNode(file, true); DomainFileNode fileNode = findDomainFileNode(file, true);
if (fileNode != null) { if (fileNode != null) {
fileNode.refresh(); fileNode.refresh();
@ -141,10 +166,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFolderAdded(DomainFolder folder) { public void domainFolderAdded(DomainFolder folder) {
String folderName = folder.getName(); String folderName = folder.getName();
DomainFolder parentFolder = folder.getParent(); DomainFolder parentFolder = folder.getParent();
updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName), updateLinkedContent(parentFolder.getPathname(), p -> addFolderNode(p, folderName),
ltn -> ltn.refreshLinks(folderName)); ltn -> ltn.refreshLinks(folderName));
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true); DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
if (folderNode != null && folderNode.isLoaded()) { if (folderNode != null && folderNode.isLoaded()) {
addFolderNode(folderNode, folderName); addFolderNode(folderNode, folderName);
@ -153,7 +180,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFolderRemoved(DomainFolder parent, String name) { public void domainFolderRemoved(DomainFolder parent, String name) {
updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
DomainFolderNode folderNode = findDomainFolderNode(parent, true); DomainFolderNode folderNode = findDomainFolderNode(parent, true);
if (folderNode != null) { if (folderNode != null) {
updateChildren(folderNode); updateChildren(folderNode);
@ -162,17 +191,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFolderRenamed(DomainFolder folder, String oldName) { public void domainFolderRenamed(DomainFolder folder, String oldName) {
updateLinkedContent(folder.getParent(), p -> {
updateChildren(p); domainFolderMoved(folder.getParent().getPathname(), oldName, folder);
addFolderNode(p, folder.getName());
}, ltn -> {
ltn.refreshLinks(oldName);
ltn.refreshLinks(folder.getName());
});
DomainFolder parent = folder.getParent();
skipLinkUpdate = true; skipLinkUpdate = true;
try { try {
domainFolderRemoved(parent, oldName); domainFolderRemoved(folder.getParent(), oldName);
domainFolderAdded(folder); domainFolderAdded(folder);
} }
finally { finally {
@ -182,9 +206,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) { public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) {
domainFolderMoved(oldParent.getPathname(), folder.getName(), folder);
skipLinkUpdate = true;
try {
domainFolderRemoved(oldParent, folder.getName()); domainFolderRemoved(oldParent, folder.getName());
domainFolderAdded(folder); domainFolderAdded(folder);
} }
finally {
skipLinkUpdate = false;
}
}
@Override @Override
public void domainFolderSetActive(DomainFolder folder) { public void domainFolderSetActive(DomainFolder folder) {
@ -194,6 +227,36 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
} }
/**
* Following a folder move or rename where only a single notification is provided this
* method should be used to propogate link related updates which may refer to the affected
* folder or its children. This method is invoked recursively for all child folders.
* @param oldParentPath folder's old parent path
* @param oldName folder's previous name
* @param folder folder instance following rename
*/
private void domainFolderMoved(String oldParentPath, String oldName, DomainFolder folder) {
String oldFolderPathname = LocalFileSystem.getPath(oldParentPath, oldName);
// Recurse over all child folders.
for (DomainFolder childFolder : folder.getFolders()) {
domainFolderMoved(oldFolderPathname, childFolder.getName(), childFolder);
}
// Refresh links to old placement
updateLinkedContent(oldParentPath, null, ltn -> {
ltn.refreshLinks(oldName);
});
// Refresh links to new placement
String newName = folder.getName();
updateLinkedContent(folder.getParent().getPathname(), p -> addFolderNode(p, newName),
ltn -> {
ltn.refreshLinks(newName);
});
}
// //
// Helper methods // Helper methods
// //
@ -210,9 +273,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) { private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
Msg.debug(this, "addFileNode: " + node.getPathname() + " " + fileName + " " +
Boolean.toString(isFolderLink));
if (node.isLeaf() || !node.isLoaded()) { if (node.isLeaf() || !node.isLoaded()) {
return; return;
} }
// Check for existance of file by that name // Check for existance of file by that name
DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName, DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE); isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
@ -234,6 +301,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (node.isLeaf() || !node.isLoaded()) { if (node.isLeaf() || !node.isLoaded()) {
return; return;
} }
// Check for existance of folder by that name // Check for existance of folder by that name
if (node.getChild(folderName, NodeType.FOLDER) != null) { if (node.getChild(folderName, NodeType.FOLDER) != null) {
return; return;
@ -262,6 +330,20 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
return findDomainFolderNode(folderPath, lazy); return findDomainFolderNode(folderPath, lazy);
} }
// private List<String> getPathAsList(String pathname) {
// ArrayList<String> folderPath = new ArrayList<String>();
// String[] pathSplit = pathname.split(FileSystem.SEPARATOR);
// for (int i = 1; i < pathSplit.length; i++) {
// folderPath.add(pathSplit[i]);
// }
// return folderPath;
// }
//
// private DomainFolderNode findDomainFolderNode(String pathname, boolean lazy) {
// List<String> folderPath = getPathAsList(pathname);
// return findDomainFolderNode(folderPath, lazy);
// }
private DomainFolderNode findDomainFolderNode(List<String> folderPath, boolean lazy) { private DomainFolderNode findDomainFolderNode(List<String> folderPath, boolean lazy) {
DomainFolderNode folderNode = root; DomainFolderNode folderNode = root;
for (String name : folderPath) { for (String name : folderPath) {
@ -284,6 +366,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (lazy && !folderNode.isLoaded()) { if (lazy && !folderNode.isLoaded()) {
return null; // not visited return null; // not visited
} }
boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink(); boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
return (DomainFileNode) folderNode.getChild(domainFile.getName(), return (DomainFileNode) folderNode.getChild(domainFile.getName(),
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE); isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
@ -334,40 +417,51 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override @Override
public void treeStructureChanged(TreeModelEvent e) { public void treeStructureChanged(TreeModelEvent e) {
// This is used when an existing node is loaded to register all of its link-file children // NOTE: We have seen getTreePath return null in the test environment
// since the occurance of treeNodesChanged cannot be relied upon for notification of // immediately before ChangeManager disposal
// these existing children.
TreePath treePath = e.getTreePath(); TreePath treePath = e.getTreePath();
if (treePath == null) {
return; Object[] changedChildren = e.getChildren();
if (changedChildren != null) {
for (Object child : changedChildren) {
treeNodeChanged(child, true);
} }
Object treeNode = treePath.getLastPathComponent(); }
else if (treePath != null) {
treeNodeChanged(treePath.getLastPathComponent(), true);
}
}
private void treeNodeChanged(Object treeNode, boolean processLoadedChildren) {
if (!(treeNode instanceof DataTreeNode dataTreeNode)) { if (!(treeNode instanceof DataTreeNode dataTreeNode)) {
return; return;
} }
if (!dataTreeNode.isLoaded()) {
return; if (treeNode instanceof DomainFileNode fileNode) {
}
// Register all visible link-file nodes
for (GTreeNode child : dataTreeNode.getChildren()) {
if (child instanceof DomainFileNode fileNode) {
if (fileNode.getDomainFile().isLink()) {
addLinkFile(fileNode); addLinkFile(fileNode);
} }
}
} // TODO: Not sure we need the following code
// if (processLoadedChildren && dataTreeNode.isLoaded()) {
// for (GTreeNode node : dataTreeNode.getChildren()) {
// treeNodeChanged(node, true);
// }
// }
} }
@Override @Override
public void treeNodesChanged(TreeModelEvent e) { public void treeNodesChanged(TreeModelEvent e) {
// This is used to register link-file nodes which may be added to the tree as a result Object[] changedChildren = e.getChildren();
// of changes to the associated project data. if (changedChildren != null) {
for (Object child : changedChildren) {
Object treeNode = e.getTreePath().getLastPathComponent(); treeNodeChanged(child, false);
if (treeNode instanceof DomainFileNode fileNode) { }
addLinkFile(fileNode); }
else {
treeNodeChanged(e.getTreePath().getLastPathComponent(), false);
} }
} }
@ -385,6 +479,19 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
// Link tracking tree update support // Link tracking tree update support
// //
private void addLoadedChildren(DataTreeNode node) {
if (!node.isLoaded()) {
return;
}
for (GTreeNode child : node.getChildren()) {
if (child instanceof DomainFileNode fileNode) {
addLinkFile(fileNode);
}
}
}
/** /**
* Update link tree if the specified {@code domainFileNode} corresponds to an link-file * Update link tree if the specified {@code domainFileNode} corresponds to an link-file
* which has an internal link-path which links to either a file or folder within the same * which has an internal link-path which links to either a file or folder within the same
@ -403,6 +510,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
try { try {
String linkPath = LinkHandler.getAbsoluteLinkPath(file); String linkPath = LinkHandler.getAbsoluteLinkPath(file);
if (linkPath == null) { if (linkPath == null) {
return; return;
@ -420,6 +528,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (isFolderLink) { if (isFolderLink) {
folderLinkNode.addLinkedFolder(domainFileNode); folderLinkNode.addLinkedFolder(domainFileNode);
addLoadedChildren(domainFileNode);
} }
else { else {
folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode); folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode);
@ -439,20 +548,35 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
* once if a {@code LinkedTreeNode} is found which corresponds to the specified * once if a {@code LinkedTreeNode} is found which corresponds to the specified
* {@code parentFolder}. This allows targeted refresh of link-files. * {@code parentFolder}. This allows targeted refresh of link-files.
* *
* @param parentFolder a parent folder which relates to a change * @param parentFolderPath the parent folder path which relates to a change
* @param folderNodeConsumer optional consumer which will be invoked for each loaded parent * @param folderNodeConsumer optional consumer which will be invoked for each loaded parent
* tree node which is a linked-reflection of the specified {@code parentFolder}. If null is * tree node which is a linked-reflection of the specified {@code parentFolder}. If null is
* specified for this consumer a general update will be performed to remove any missing nodes. * specified for this consumer a general update will be performed to remove any missing nodes.
* @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode} * @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
* is found which corresponds to the specified {@code parentFolder}. * is found which corresponds to the specified {@code parentFolder}.
*/ */
void updateLinkedContent(DomainFolder parentFolder, Consumer<DataTreeNode> folderNodeConsumer, private void updateLinkedContent(String parentFolderPath,
Consumer<LinkedTreeNode> linkNodeConsumer) { Consumer<DataTreeNode> folderNodeConsumer, Consumer<LinkedTreeNode> linkNodeConsumer) {
if (!Swing.isSwingThread()) {
throw new RuntimeException(
"Listener and all node updates must operate in Swing thread");
}
if (skipLinkUpdate) { if (skipLinkUpdate) {
return; return;
} }
String pathname = parentFolder.getPathname();
String[] pathElements = pathname.split("/"); // NOTE: This method must track those paths which have been refreshed to avoid the
// possibility of infinite recursion when circular links exist.
boolean clearRefreshedTrackingSet = false;
if (refreshedTrackingSet == null) {
refreshedTrackingSet = new HashSet<>();
clearRefreshedTrackingSet = true;
}
try {
String[] pathElements = parentFolderPath.split("/");
LinkedTreeNode folderLinkNode = linkTreeRoot; LinkedTreeNode folderLinkNode = linkTreeRoot;
folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer); folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
for (int i = 1; i < pathElements.length; i++) { for (int i = 1; i < pathElements.length; i++) {
@ -469,9 +593,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
linkNodeConsumer.accept(folderLinkNode); linkNodeConsumer.accept(folderLinkNode);
} }
} }
finally {
if (clearRefreshedTrackingSet) {
refreshedTrackingSet = null;
}
}
}
private class LinkedTreeNode { private class LinkedTreeNode {
// NOTE: The use of HashSet to track LinkedTreeNodes relies on identity hashcode and
// same instance for equality.
private final LinkedTreeNode parent; private final LinkedTreeNode parent;
private final String name; private final String name;
@ -491,7 +624,14 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
boolean updateThisNode = subFolderPathIndex >= pathElements.length; boolean updateThisNode = subFolderPathIndex >= pathElements.length;
for (DomainFileNode folderLink : folderLinks) { Iterator<DomainFileNode> folderLinkIter = folderLinks.iterator();
while (folderLinkIter.hasNext()) {
DomainFileNode folderLink = folderLinkIter.next();
if (folderLink.getParent() == null) {
// Remove disposed link node
folderLinkIter.remove();
}
if (!folderLink.isLoaded()) { if (!folderLink.isLoaded()) {
continue; continue;
@ -529,6 +669,25 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
private void refreshLinks(String childName) { private void refreshLinks(String childName) {
String childPathName = LocalFileSystem.getPath(getPathname(), childName);
if (!refreshedTrackingSet.add(childPathName)) {
return;
}
// If links are defined be sure to visit DomainFolder so that we pickup on change
// events even if not visible within tree.
// TODO: Should no longer be needed after changes were made to force domain folder events
// which would affect discovered link-files
// if (!folderMap.isEmpty()) {
// String path = LocalFileSystem.getPath(getPathname(), childName);
// DomainFolder folder =
// projectData.getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
// if (folder != null) {
// folder.getFolders(); // forced visit to folder
// }
// }
// We are forced to refresh file-links and folder-links since a folder-link may be // We are forced to refresh file-links and folder-links since a folder-link may be
// referencing another folder-link file and not the final referenced folder. // referencing another folder-link file and not the final referenced folder.
if (refreshFileLinks(childName) || refreshFolderLinks(childName)) { if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
@ -537,10 +696,28 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
private boolean refreshFolderLinks(String folderName) { private boolean refreshFolderLinks(String folderName) {
LinkedTreeNode linkedTreeNode = folderMap.get(folderName); LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
if (linkedTreeNode != null) { if (linkedTreeNode != null) {
refresh(linkedTreeNode.folderLinks); refresh(linkedTreeNode.folderLinks);
return linkedTreeNode.folderLinks.isEmpty(); boolean removed = linkedTreeNode.folderLinks.isEmpty();
// Refresh all file links refering to files within this folder
Collection<Set<DomainFileNode>> linkedFileSets =
linkedTreeNode.linkedFilesMap.values();
if (!linkedFileSets.isEmpty()) {
Iterator<Set<DomainFileNode>> iterator = linkedFileSets.iterator();
while (iterator.hasNext()) {
Set<DomainFileNode> linkFileSet = iterator.next();
refresh(linkFileSet);
if (linkFileSet.isEmpty()) {
iterator.remove();
removed = true;
}
}
}
return removed;
} }
return false; return false;
} }
@ -557,12 +734,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
return false; return false;
} }
/**
* Add or get existing named child folder node for this folder node
* @param folderName child folder node
* @return new or existing named child folder node
*/
private LinkedTreeNode addFolder(String folderName) { private LinkedTreeNode addFolder(String folderName) {
return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n)); return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n));
} }
private void addLinkedFolder(DomainFileNode folderLink) { /**
folderLinks.add(folderLink); * Add a folder-link which references this folder node
* @param folderLink link which references this folder node
* @return true if the set did not already contain the specified folderLink
*/
private boolean addLinkedFolder(DomainFileNode folderLink) {
return folderLinks.add(folderLink);
} }
private void addLinkedFile(String fileName, DomainFileNode fileLink) { private void addLinkedFile(String fileName, DomainFileNode fileLink) {
@ -579,23 +766,27 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
} }
} }
private static void refresh(Set<DomainFileNode> linkFiles) { private void refresh(Set<DomainFileNode> linkFiles) {
List<DomainFileNode> purgeList = null; Iterator<DomainFileNode> linkFileIter = linkFiles.iterator();
for (DomainFileNode fileLink : linkFiles) { while (linkFileIter.hasNext()) {
DomainFile file = fileLink.getDomainFile(); DomainFileNode fileLink = linkFileIter.next();
// Perform lazy purge of missing link files if (fileLink.getParent() == null || !fileLink.getDomainFile().isLink()) {
if (!file.isLink()) { linkFileIter.remove();
if (purgeList == null) {
purgeList = new ArrayList<>();
}
purgeList.add(fileLink);
} }
else { else {
fileLink.refresh(); fileLink.refresh();
GTreeNode linkParent = fileLink.getParent();
if (linkParent instanceof DomainFolderNode linkParentNode) {
// TODO: What about LinkedDomainFolders?
ChangeManager.this.updateLinkedContent(linkParentNode.getPathname(), fn -> {
/* do nothing */ }, ltn -> {
ltn.refreshLinks(fileLink.getName());
});
} }
} }
if (purgeList != null) {
linkFiles.removeAll(purgeList);
} }
} }

View file

@ -142,6 +142,33 @@ public class DataTree extends GTree {
} }
else if (node instanceof DomainFileNode fileNode) { else if (node instanceof DomainFileNode fileNode) {
if (fileNode.isFolderLink()) { if (fileNode.isFolderLink()) {
folder = getLinkedFolder(fileNode);
}
else {
// Handle normal file cases where we return node's parent folder
GTreeNode parent = node.getParent();
if (parent instanceof DomainFolderNode folderNode) {
folder = folderNode.getDomainFolder();
}
else if (parent instanceof DomainFileNode parentFileNode) {
folder = getLinkedFolder(parentFileNode);
}
}
}
if (folder instanceof LinkedDomainFolder linkedFolder) {
// Resolve linked internal folder to its real folder
try {
folder = linkedFolder.getRealFolder();
}
catch (IOException e) {
folder = null;
}
}
return folder;
}
private static DomainFolder getLinkedFolder(DomainFileNode fileNode) {
// Handle case where file node corresponds to a folder-link. // Handle case where file node corresponds to a folder-link.
// Folder-Link status needs to be checked to ensure it corresponds to a folder // Folder-Link status needs to be checked to ensure it corresponds to a folder
// internal to the same project. // internal to the same project.
@ -154,25 +181,6 @@ public class DataTree extends GTree {
return null; return null;
} }
// Get linked folder - status check ensures null will not be returned // Get linked folder - status check ensures null will not be returned
folder = linkInfo.getLinkedFolder(); return linkInfo.getLinkedFolder();
}
else {
// Handle normal file cases where we return node's parent folder
GTreeNode parent = node.getParent();
if (parent instanceof DomainFolderNode folderNode) {
folder = folderNode.getDomainFolder();
}
}
}
if (folder instanceof LinkedDomainFolder linkedFolder) {
// Resolve linked internal folder to its real folder
try {
folder = linkedFolder.getRealFolder();
}
catch (IOException e) {
folder = null;
}
}
return folder;
} }
} }

View file

@ -174,7 +174,9 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
private List<GTreeNode> getDomainParentNodes(List<GTreeNode> nodeList) { private List<GTreeNode> getDomainParentNodes(List<GTreeNode> nodeList) {
List<GTreeNode> parentList = new ArrayList<>(); List<GTreeNode> parentList = new ArrayList<>();
for (GTreeNode node : nodeList) { for (GTreeNode node : nodeList) {
if (!node.isLeaf()) { if (node instanceof DomainFolderNode) {
// We want to ensure we treat link-file node as not being a parent
// for this operation.
parentList.add(node); parentList.add(node);
} }
} }

View file

@ -94,6 +94,11 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
*/ */
public abstract ProjectData getProjectData(); public abstract ProjectData getProjectData();
/**
* @returns domain folder/file pathname within project
*/
public abstract String getPathname();
@Override @Override
public abstract int compareTo(GTreeNode node); public abstract int compareTo(GTreeNode node);

View file

@ -46,7 +46,12 @@ public class DomainFileNode extends DataTreeNode {
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file"); private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
private static final String RIGHT_ARROW = "\u2192"; private static final String RIGHT_ARROW = "\u2192";
// NOTE: We must ensure anything used by sort comparator remains fixed
private final DomainFile domainFile; private final DomainFile domainFile;
private final boolean isFolderLink;
private LinkFileInfo linkInfo;
private boolean isLeaf;
private volatile String displayName; // name displayed in the tree private volatile String displayName; // name displayed in the tree
private volatile Icon icon = UNKNOWN_FILE_ICON; private volatile Icon icon = UNKNOWN_FILE_ICON;
@ -54,15 +59,14 @@ public class DomainFileNode extends DataTreeNode {
private volatile String toolTipText; private volatile String toolTipText;
private AtomicInteger refreshCount = new AtomicInteger(); private AtomicInteger refreshCount = new AtomicInteger();
private boolean isLeaf = true;
private LinkFileInfo linkInfo;
private DomainFileFilter filter; // relavent when expand folder-link which is a file private DomainFileFilter filter; // relavent when expand folder-link which is a file
private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa"); private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
DomainFileNode(DomainFile domainFile, DomainFileFilter filter) { DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
this.domainFile = domainFile; this.domainFile = domainFile;
this.linkInfo = domainFile.getLinkInfo(); linkInfo = domainFile.getLinkInfo();
isFolderLink = linkInfo != null && linkInfo.isFolderLink();
this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER; this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
displayName = domainFile.getName(); displayName = domainFile.getName();
refresh(); refresh();
@ -84,6 +88,11 @@ public class DomainFileNode extends DataTreeNode {
return domainFile; return domainFile;
} }
@Override
public String getPathname() {
return domainFile.getPathname();
}
@Override @Override
public boolean isLeaf() { public boolean isLeaf() {
return isLeaf; return isLeaf;
@ -103,15 +112,11 @@ public class DomainFileNode extends DataTreeNode {
* @return true if file is a folder-link * @return true if file is a folder-link
*/ */
public boolean isFolderLink() { public boolean isFolderLink() {
if (linkInfo != null) { return isFolderLink;
return linkInfo.isFolderLink();
}
return false;
} }
/** /**
* Get linked folder which corresponds to this folder-link * Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}).
* (see {@link #isFolderLink()}).
* @return linked folder or null if this is not a folder-link * @return linked folder or null if this is not a folder-link
*/ */
LinkedDomainFolder getLinkedFolder() { LinkedDomainFolder getLinkedFolder() {
@ -214,12 +219,20 @@ public class DomainFileNode extends DataTreeNode {
private void doRefresh() { private void doRefresh() {
isLeaf = true; isLeaf = true;
linkInfo = null; LinkFileInfo updatedLinkInfo = domainFile.getLinkInfo();
boolean brokenLink = false; boolean brokenLink = false;
List<String> linkErrors = null; List<String> linkErrors = null;
if (domainFile.isLink()) {
linkInfo = domainFile.getLinkInfo(); if (isFolderLink != (updatedLinkInfo != null && updatedLinkInfo.isFolderLink())) {
// Linked-folder node state changed. Since this alters sort order we can't handle this.
// Such a DomainFile state change must be handled by the ChangeManager
brokenLink = true;
linkErrors = List.of("Unsupported folder-link transition");
}
else {
linkInfo = updatedLinkInfo;
if (linkInfo != null) {
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
LinkStatus linkStatus = LinkStatus linkStatus =
LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg)); LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
@ -227,7 +240,7 @@ public class DomainFileNode extends DataTreeNode {
if (brokenLink) { if (brokenLink) {
linkErrors = errors; linkErrors = errors;
} }
else if (isFolderLink()) { else if (isFolderLink) {
if (linkStatus == LinkStatus.INTERNAL) { if (linkStatus == LinkStatus.INTERNAL) {
isLeaf = false; isLeaf = false;
} }
@ -237,11 +250,12 @@ public class DomainFileNode extends DataTreeNode {
} }
} }
} }
if (isLeaf) {
unloadChildren();
} }
// We must always unload any children since a leaf has no children and a folder-link
// may be transitioning to a state where its children may need to be re-loaded.
unloadChildren();
displayName = getFormattedDisplayName(); displayName = getFormattedDisplayName();
toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors)); toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors));
@ -289,7 +303,25 @@ public class DomainFileNode extends DataTreeNode {
private String getFormattedLinkPath() { private String getFormattedLinkPath() {
String linkPath = linkInfo != null ? linkInfo.getLinkPath() : null; String linkPath = null;
// If link-file is a LinkedDomainFile we must always show an absolute link-path since
// relative paths are relative to the real file's location and it would be rather confusing
// to show as relative
if (domainFile instanceof LinkedDomainFile) {
try {
// will return GhidraURL or absolute internal path
linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
}
catch (IOException e) {
// attempt to use stored path, although it may fail as well
linkPath = linkInfo.getLinkPath();
}
}
else if (linkInfo != null) {
linkPath = linkInfo.getLinkPath();
}
if (GhidraURL.isGhidraURL(linkPath)) { if (GhidraURL.isGhidraURL(linkPath)) {
try { try {
URL url = new URL(linkPath); URL url = new URL(linkPath);

View file

@ -20,6 +20,7 @@ import java.util.List;
import javax.swing.Icon; import javax.swing.Icon;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.util.*; import ghidra.util.*;
@ -48,6 +49,7 @@ public class DomainFolderNode extends DataTreeNode {
private boolean isEditable; private boolean isEditable;
DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) { DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) {
this.domainFolder = domainFolder; this.domainFolder = domainFolder;
this.filter = filter; this.filter = filter;
@ -73,6 +75,11 @@ public class DomainFolderNode extends DataTreeNode {
return domainFolder; return domainFolder;
} }
@Override
public String getPathname() {
return domainFolder.getPathname();
}
/** /**
* Returns true if this node has no children. * Returns true if this node has no children.
*/ */

View file

@ -112,17 +112,25 @@ public final class LocalTreeNodeHandler
} }
private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) { private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) {
try {
// NOTE: destFolder should be real folder and not linked-folder
// NOTE: We may have issues since checks are not based on canonical paths // NOTE: We may have issues since checks are not based on canonical paths
if (draggedNode instanceof DomainFolderNode folderNode) { if (draggedNode instanceof DomainFolderNode folderNode) {
// This also checks cases where src/dest projects are using the same repository. // This also checks cases where src/dest projects are using the same repository.
// Unfortunately, it will also prevent cases where shared-project folder // Unfortunately, it will also prevent cases where shared-project folder
// does not contain versioned content and could actually be allowed. // does not contain versioned content and could actually be allowed.
DomainFolder folder = folderNode.getDomainFolder(); DomainFolder folder = folderNode.getDomainFolder();
if (folder instanceof LinkedDomainFolder linkedFolder) {
folder = linkedFolder.getRealFolder();
}
return !folder.isSameOrAncestor(destFolder); return !folder.isSameOrAncestor(destFolder);
} }
if (draggedNode instanceof DomainFileNode fileNode) { if (draggedNode instanceof DomainFileNode fileNode) {
DomainFolder folder = fileNode.getDomainFile().getParent();
DomainFile file = fileNode.getDomainFile(); DomainFile file = fileNode.getDomainFile();
if (file instanceof LinkedDomainFile linkedFile) {
file = linkedFile.getRealFile();
}
DomainFolder folder = file.getParent();
if (file.isVersioned()) { if (file.isVersioned()) {
// This also checks cases where src/dest projects are using the same repository. // This also checks cases where src/dest projects are using the same repository.
return !folder.isSame(destFolder); return !folder.isSame(destFolder);
@ -130,6 +138,10 @@ public final class LocalTreeNodeHandler
DomainFile destFile = destFolder.getFile(file.getName()); DomainFile destFile = destFolder.getFile(file.getName());
return destFile == null || !destFile.equals(file); return destFile == null || !destFile.equals(file);
} }
}
catch (IOException e) {
// ignore
}
return false; return false;
} }
@ -165,6 +177,9 @@ public final class LocalTreeNodeHandler
} }
try { try {
if (file instanceof LinkedDomainFile linkedFile) {
file = linkedFile.getRealFile();
}
file.moveTo(destFolder); file.moveTo(destFolder);
} }
catch (IOException e) { catch (IOException e) {
@ -186,6 +201,9 @@ public final class LocalTreeNodeHandler
} }
try { try {
if (sourceFolder instanceof LinkedDomainFolder linkedFolder) {
sourceFolder = linkedFolder.getRealFolder();
}
sourceFolder.moveTo(destFolder); sourceFolder.moveTo(destFolder);
} }
catch (DuplicateFileException dfe) { catch (DuplicateFileException dfe) {

View file

@ -17,8 +17,7 @@ package ghidra.framework.main.projectdata.actions;
import java.awt.Component; import java.awt.Component;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.*;
import java.util.Set;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import docking.widgets.OptionDialogBuilder; import docking.widgets.OptionDialogBuilder;
@ -65,10 +64,21 @@ public class DeleteProjectFilesTask extends Task {
initializeMonitor(monitor); initializeMonitor(monitor);
deleteFiles(selectedFiles, monitor); Set<DomainFile> resolvedFiles = resolveLinkedFiles(selectedFiles);
deleteFolders(selectedFolders, monitor); Set<DomainFolder> resolvedFolders = resolveLinkedFolders(selectedFolders);
try {
deleteFiles(resolvedFiles, monitor);
deleteFolders(resolvedFolders, monitor);
}
catch (CancelledException e) {
// ignore
}
}
public void showReport() {
statistics.showReport(parent); statistics.showReport(parent);
} }
@ -77,60 +87,105 @@ public class DeleteProjectFilesTask extends Task {
monitor.initialize(statistics.getFileCount()); monitor.initialize(statistics.getFileCount());
} }
private void deleteFiles(Set<DomainFile> files, TaskMonitor monitor) { /**
try { * Domain file comparator for use in establishing order of file removal.
* All real files (i.e., non-link-files) must be removed before link-files
* afterwhich depth-first applies.
*/
private static final Comparator<DomainFile> FILE_PATH_COMPARATOR =
new Comparator<DomainFile>() {
@Override
public int compare(DomainFile o1, DomainFile o2) {
// Ensure that non-link-files occur first in sorted list
boolean isLink1 = o1.isLink();
boolean isLink2 = o2.isLink();
if (isLink1) {
if (!isLink2) {
return 1;
}
}
else if (isLink2) {
return -1;
}
// Compare paths to ensure deeper paths occur first in sorted list
String path1 = o1.getPathname();
String path2 = o2.getPathname();
return path2.compareTo(path1);
}
};
private Set<DomainFile> resolveLinkedFiles(Set<DomainFile> files) {
Set<DomainFile> resolvedFiles = new HashSet<>();
for (DomainFile file : files) { for (DomainFile file : files) {
monitor.checkCancelled(); // If file is contained within a linked-folder (LinkedDomainFile) we need to
deleteFile(file); // use the actual linked file. Since we should be dealing with internally
monitor.incrementProgress(1); // linked content IOExceptions are unexpected.
} if (file instanceof LinkedDomainFile linkedFile) {
}
catch (CancelledException e) {
// just return so that statistics for what completed can be displayed
}
}
private void deleteFolders(Set<DomainFolder> folders, TaskMonitor monitor) {
try { try {
file = linkedFile.getRealFile();
}
catch (IOException e) {
continue; // Skip file if unable to resolve
}
}
resolvedFiles.add(file);
}
return resolvedFiles;
}
private Set<DomainFolder> resolveLinkedFolders(Set<DomainFolder> folders) {
Set<DomainFolder> resolvedFolders = new HashSet<>();
for (DomainFolder folder : folders) { for (DomainFolder folder : folders) {
// If folder is a linked-folder (LinkedDomainFolder) we need to
// use the actual linked folder. Since we should be dealing with internally
// linked content IOExceptions are unexpected.
if (folder instanceof LinkedDomainFolder linkedFolder) {
try {
folder = linkedFolder.getRealFolder();
}
catch (IOException e) {
continue; // skip file if unable to resolve
}
}
resolvedFolders.add(folder);
}
return resolvedFolders;
}
private void deleteFiles(Set<DomainFile> files, TaskMonitor monitor) throws CancelledException {
ArrayList<DomainFile> sortedFiles = new ArrayList<>(files);
Collections.sort(sortedFiles, FILE_PATH_COMPARATOR);
for (DomainFile file : sortedFiles) {
monitor.checkCancelled();
deleteFile(file, monitor);
}
}
private void deleteFolders(Set<DomainFolder> folders, TaskMonitor monitor)
throws CancelledException {
for (DomainFolder folder : folders) {
monitor.checkCancelled();
deleteFolder(folder, monitor); deleteFolder(folder, monitor);
} }
} }
catch (CancelledException e) {
// just return so that statistics for what completed can be displayed
}
}
private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException { private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException {
while (folder instanceof LinkedDomainFolder linkedFolder) {
if (linkedFolder.isLinked()) {
throw new IllegalArgumentException(
"Linked-folder's originating file-link should have been removed instead: " +
linkedFolder.getPathname());
}
try {
folder = linkedFolder.getRealFolder();
}
catch (IOException e) {
Msg.error(this, "Error following linked-folder: " + e.getMessage() + "\n" +
folder.getPathname());
return;
}
}
for (DomainFolder subFolder : folder.getFolders()) { for (DomainFolder subFolder : folder.getFolders()) {
monitor.checkCancelled(); monitor.checkCancelled();
if (!selectedFolders.contains(subFolder)) {
deleteFolder(subFolder, monitor); deleteFolder(subFolder, monitor);
} }
}
for (DomainFile file : folder.getFiles()) { for (DomainFile file : folder.getFiles()) {
monitor.checkCancelled(); monitor.checkCancelled();
if (!selectedFiles.contains(file)) { if (!selectedFiles.contains(file)) {
deleteFile(file); deleteFile(file, monitor);
monitor.incrementProgress(1); monitor.incrementProgress(1);
} }
} }
@ -149,7 +204,13 @@ public class DeleteProjectFilesTask extends Task {
} }
} }
private void deleteFile(DomainFile file) throws CancelledException { private void deleteFile(DomainFile file, TaskMonitor monitor) throws CancelledException {
if (!file.exists()) {
return;
}
try {
if (file.isOpen()) { if (file.isOpen()) {
statistics.incrementFileInUse(); statistics.incrementFileInUse();
showFileInUseDialog(file); showFileInUseDialog(file);
@ -184,7 +245,6 @@ public class DeleteProjectFilesTask extends Task {
} }
} }
try {
file.delete(); file.delete();
statistics.incrementDeleted(); statistics.incrementDeleted();
} }
@ -198,6 +258,9 @@ public class DeleteProjectFilesTask extends Task {
throw new CancelledException(); throw new CancelledException();
} }
} }
finally {
monitor.increment();
}
} }
private int showConfirmDeleteVersionedDialog(DomainFile file) { private int showConfirmDeleteVersionedDialog(DomainFile file) {

View file

@ -31,6 +31,8 @@ class FileCountStatistics {
private int deleted; private int deleted;
FileCountStatistics(int fileCount) { FileCountStatistics(int fileCount) {
// NOTE: Do the possibility of file duplication through the selection of linked-folder
// content, this count is an estimate only
this.fileCount = fileCount; this.fileCount = fileCount;
} }
@ -72,9 +74,10 @@ class FileCountStatistics {
public void showReport(Component parent) { public void showReport(Component parent) {
// don't show results if only one file processed. // don't show results if only one file processed.
if (getTotalProcessed() == 1) { if (fileCount == 1 && getTotalProcessed() == 1) {
return; return;
} }
// don't show results if all selected files deleted // don't show results if all selected files deleted
if (deleted == fileCount) { if (deleted == fileCount) {
return; return;
@ -97,20 +100,24 @@ class FileCountStatistics {
builder.append("<tr><td>In Use: </td><td>").append(fileInUse).append("</td></tr>"); builder.append("<tr><td>In Use: </td><td>").append(fileInUse).append("</td></tr>");
} }
if (versionedDeclined > 0) { if (versionedDeclined > 0) {
builder.append("<tr><td> Versioned: </td><td>").append(versionedDeclined).append( builder.append("<tr><td> Versioned: </td><td>")
"</td></tr>"); .append(versionedDeclined)
.append("</td></tr>");
} }
if (checkedOutVersioned > 0) { if (checkedOutVersioned > 0) {
builder.append("<tr><td>Checked-out: </td><td>").append(checkedOutVersioned).append( builder.append("<tr><td>Checked-out: </td><td>")
"</td></tr>"); .append(checkedOutVersioned)
.append("</td></tr>");
} }
if (readOnlySkipped > 0) { if (readOnlySkipped > 0) {
builder.append("<tr><td>Read only: </td><td>").append(readOnlySkipped).append( builder.append("<tr><td>Read only: </td><td>")
"</td></tr>"); .append(readOnlySkipped)
.append("</td></tr>");
} }
if (generalFailure > 0) { if (generalFailure > 0) {
builder.append("<tr><td>Other: </td><td>").append(generalFailure).append( builder.append("<tr><td>Other: </td><td>")
"</td></tr>"); .append(generalFailure)
.append("</td></tr>");
} }
builder.append("</table>"); builder.append("</table>");
} }

View file

@ -58,13 +58,17 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
// Confirm the delete *without* using a task so that do not have 2 dialogs showing // Confirm the delete *without* using a task so that do not have 2 dialogs showing
int fileCount = countTask.getFileCount(); int fileCount = countTask.getFileCount();
if (!confirmDelete(fileCount, files, context.getComponent())) { if (!confirmDelete(fileCount, files, folders, context.getComponent())) {
return; return;
} }
// Task 2 - perform the delete--this could take a while // Task 2 - perform the delete--this could take a while
DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount); DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount);
TaskLauncher.launch(deleteTask); TaskLauncher.launch(deleteTask);
if (!deleteTask.isCancelled()) {
deleteTask.showReport();
}
} }
DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set<DomainFile> files, DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set<DomainFile> files,
@ -72,9 +76,10 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
return new DeleteProjectFilesTask(folders, files, fileCount, context.getComponent()); return new DeleteProjectFilesTask(folders, files, fileCount, context.getComponent());
} }
private boolean confirmDelete(int fileCount, Set<DomainFile> files, Component parent) { private boolean confirmDelete(int fileCount, Set<DomainFile> files, Set<DomainFolder> folders,
Component parent) {
String message = getMessage(fileCount, files); String message = getMessage(fileCount, files, folders);
OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message); OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message);
int choice = builder.addOption("OK") int choice = builder.addOption("OK")
.addCancel() .addCancel()
@ -83,28 +88,25 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
return choice != OptionDialog.CANCEL_OPTION; return choice != OptionDialog.CANCEL_OPTION;
} }
private String getMessage(int fileCount, Set<DomainFile> selectedFiles) { private String getMessage(int fileCount, Set<DomainFile> files, Set<DomainFolder> folders) {
if (fileCount == 0) { if (fileCount == 0) {
return "Are you sure you want to delete the selected empty folder(s)?"; return "Are you sure you want to delete the selected empty folder(s)?";
} }
if (folders.isEmpty()) {
if (fileCount == 1) { if (fileCount == 1) {
if (!selectedFiles.isEmpty()) { DomainFile file = CollectionUtils.any(files);
DomainFile file = CollectionUtils.any(selectedFiles); String type = file.getContentType();
String type = file.isLink() ? "link" : "file";
return "<html>Are you sure you want to <B><U>permanently</U></B> delete " + type + return "<html>Are you sure you want to <B><U>permanently</U></B> delete " + type +
" \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?"; " \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?";
} }
// only folders are selected, but they contain files
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " + return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " +
" selected files and folders?"; " selected files?";
} }
// multiple files selected // multiple files selected
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " + fileCount + return "<html>Are you sure you want to <B><U>permanently</U></B> delete the selected folder(s) and file(s)?";
" selected files?";
} }
@Override @Override

View file

@ -25,8 +25,7 @@ import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.framework.main.datatable.ProjectTreeContext; import ghidra.framework.main.datatable.ProjectTreeContext;
import ghidra.framework.main.datatree.*; import ghidra.framework.main.datatree.*;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.*;
import ghidra.framework.model.DomainFolder;
import ghidra.util.InvalidNameException; import ghidra.util.InvalidNameException;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
@ -48,26 +47,32 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
@Override @Override
protected boolean isEnabledForContext(T context) { protected boolean isEnabledForContext(T context) {
try {
return getFolder(context).isInWritableProject(); return getFolder(context).isInWritableProject();
} }
catch (IOException e) {
return false;
}
}
private void createNewFolder(T context) { private void createNewFolder(T context) {
DomainFolder newFolder = createNewFolderWithDefaultName(context);
DomainFolder parentFolder = getFolder(context);
DomainFolder newFolder = createNewFolderWithDefaultName(parentFolder);
GTreeNode parent = getParentNode(context); GTreeNode parent = getParentNode(context);
DataTree tree = context.getTree(); DataTree tree = context.getTree();
tree.setEditable(true); tree.setEditable(true);
tree.startEditing(parent, newFolder.getName()); tree.startEditing(parent, newFolder.getName());
} }
private DomainFolder createNewFolderWithDefaultName(DomainFolder parentFolder) { private DomainFolder createNewFolderWithDefaultName(T context) {
String name = getNewFolderName(parentFolder); String errName = "";
try { try {
DomainFolder parentFolder = getFolder(context);
String name = getNewFolderName(parentFolder);
errName = ": " + name;
return parentFolder.createFolder(name); return parentFolder.createFolder(name);
} }
catch (InvalidNameException | IOException e) { catch (InvalidNameException | IOException e) {
throw new AssertException("Unexpected Error creating new folder: " + name, e); throw new AssertException("Unexpected Error creating new folder" + errName, e);
} }
} }
@ -82,18 +87,33 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
return name; return name;
} }
private DomainFolder getFolder(T context) { private DomainFolder getFolder(T context) throws IOException {
// the following code relies on the isAddToPopup to ensure that there is exactly one // the following code relied upon by the isAddToPopup to ensure that there is exactly one
// file or folder selected // file or folder selected
DomainFolder folder = null;
if (context.getFolderCount() == 1 && context.getFileCount() == 0) { if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
return context.getSelectedFolders().get(0); folder = context.getSelectedFolders().get(0);
} }
if (context.getFileCount() == 1 && context.getFolderCount() == 0) { else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0); DomainFile file = context.getSelectedFiles().get(0);
return file.getParent(); LinkFileInfo linkInfo = file.getLinkInfo();
if (linkInfo != null && linkInfo.isFolderLink()) {
folder = linkInfo.getLinkedFolder();
} }
else {
folder = file.getParent();
}
}
if (folder instanceof LinkedDomainFolder linkedFolder) {
// Use real folder associated with linked file/folder selection
folder = linkedFolder.getRealFolder();
}
if (folder == null) {
// Use root folder if valid selection not found
DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot(); DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
return rootNode.getDomainFolder(); folder = rootNode.getDomainFolder();
}
return folder;
} }
private GTreeNode getParentNode(T context) { private GTreeNode getParentNode(T context) {
@ -104,9 +124,11 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
return context.getTree().getModelRoot(); return context.getTree().getModelRoot();
} }
if (node instanceof DomainFileNode) { if (node instanceof DomainFileNode fileNode) {
if (!fileNode.isFolderLink()) {
return node.getParent(); return node.getParent();
} }
}
return node; return node;
} }
} }

View file

@ -59,6 +59,11 @@ public interface DomainFolderChangeListener {
/** /**
* Notify listeners when a domain folder is renamed. * Notify listeners when a domain folder is renamed.
* <p>
* NOTE: Only a single event will be sent for the specific folder renamed and not its children.
* If the listener cares about the impact of this event on the folder's children it will need
* to process accordingly.
*
* @param folder folder that was renamed * @param folder folder that was renamed
* @param oldName old name of folder * @param oldName old name of folder
*/ */
@ -77,6 +82,11 @@ public interface DomainFolderChangeListener {
/** /**
* Notification that the domain folder was moved. * Notification that the domain folder was moved.
* <p>
* NOTE: Only a single event will be sent for the specific folder moved and not its children.
* If the listener cares about the impact of this event on the folder's children it will need
* to process accordingly.
*
* @param folder the folder (after move) * @param folder the folder (after move)
* @param oldParent original parent folder * @param oldParent original parent folder
*/ */

View file

@ -59,8 +59,11 @@ public interface LinkFileInfo {
* method on an {@link #isExternalLink() external-link} will cause the associated * method on an {@link #isExternalLink() external-link} will cause the associated
* project or repository to be opened and associated with the active project as a * project or repository to be opened and associated with the active project as a
* a viewed-project. The resulting folder instance will return true to the method * a viewed-project. The resulting folder instance will return true to the method
* {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links * {@link DomainFolder#isLinked()}.
* which may be chained together. *
* NOTE: This method will recurse all internal folder-links which may be chained together
* and relies on link status checks to prevent possible recursion (See
* {@link LinkHandler#getLinkFileStatus(DomainFile, Consumer)}).
* *
* @return a linked domain folder or null if not a valid folder-link. * @return a linked domain folder or null if not a valid folder-link.
*/ */
@ -71,7 +74,10 @@ public interface LinkFileInfo {
* link-file's project or a Ghidra URL. * link-file's project or a Ghidra URL.
* <P> * <P>
* If you want to ensure that a project path returned is absolute and normalized, then * If you want to ensure that a project path returned is absolute and normalized, then
* {@link #getAbsoluteLinkPath()} may be used. * {@link #getAbsoluteLinkPath()} may be used. If this corresponds to a link-file that
* implements {@link LinkedDomainFile} the absolute path form must be used to avoid treating
* as relative to the incorrect parent folder. A {@link LinkedDomainFile} can occur when
* the link-file exists within a linked-folder or subfolder.
* *
* @return associated link path * @return associated link path
*/ */

View file

@ -35,6 +35,6 @@ public interface LinkedDomainFile extends DomainFile {
* @return domain file * @return domain file
* @throws IOException if IO error occurs or file not found * @throws IOException if IO error occurs or file not found
*/ */
public DomainFile getLinkedFile() throws IOException; public DomainFile getRealFile() throws IOException;
} }

View file

@ -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.data;
import static org.junit.Assert.*;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class RelativePathTest extends AbstractGenericTest {
@Test
public void testGetRelativePath() {
// File links
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", false));
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b", "/a/b", false));
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", false));
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", false));
// Folder links
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", true));
assertEquals(".", GhidraFolderData.getRelativePath("/a/b", "/a/b", true));
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", true));
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", true));
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b/", "/a/b", true)); // See Note-1
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/", "/a/b", true));
assertEquals("c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b", true));
assertEquals("../c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b/d", true));
}
}

View file

@ -57,6 +57,8 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
// NOTE: TestDummyDomainFolder is inadequate to test against linked-folder cases
// which requires real project data support.
root = new TestDummyDomainFolder(null, "root"); root = new TestDummyDomainFolder(null, "root");
a = root.createFolder("a"); a = root.createFolder("a");
b = root.createFolder("b"); b = root.createFolder("b");
@ -488,9 +490,9 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
private void runAction() { private void runAction() {
ActionContext context = new ProjectDataContext(/*provider*/null, /*project data*/null, ActionContext context =
/*context object*/ null, CollectionUtils.asList(folders), CollectionUtils.asList(files), new ProjectDataContext(/*provider*/null, /*project data*/null, /*context object*/ null,
null, true); CollectionUtils.asList(folders), CollectionUtils.asList(files), null, true);
performAction(deleteAction, context, false); performAction(deleteAction, context, false);
waitForSwing(); waitForSwing();
} }

View file

@ -526,7 +526,6 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
// of folder or another folder-link-file at the referenced location // of folder or another folder-link-file at the referenced location
// //
String urlPath = sharedFolderURL.toExternalForm(); // will end with '/' String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/'
assertEquals(urlPath, linkInfo.getLinkPath()); assertEquals(urlPath, linkInfo.getLinkPath());
@ -593,7 +592,7 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
viewTreeHelper.getDomainFileActionContext(f1LinkFile); viewTreeHelper.getDomainFileActionContext(f1LinkFile);
URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
"Test", "/f1Link", null); "Test", "/f1Link/", null);
DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
assertNotNull("Copy action not found", copyAction); assertNotNull("Copy action not found", copyAction);

View file

@ -0,0 +1,420 @@
/* ###
* 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.main.datatree;
import static org.junit.Assert.*;
import java.util.function.BooleanSupplier;
import org.junit.*;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.listing.Program;
import ghidra.test.*;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
public class ProjectDataTreeTest extends AbstractGhidraHeadedIntegrationTest {
private FrontEndTestEnv env;
private DomainFile programAFile;
private Program program;
@Before
public void setUp() throws Exception {
env = new FrontEndTestEnv();
program = ToyProgramBuilder.buildSimpleProgram("foo", this);
DomainFolder rootFolder = env.getRootFolder();
programAFile = rootFolder.getFile("Program_A");
assertNotNull(programAFile);
}
@After
public void tearDown() throws Exception {
if (program != null) {
program.release(this);
}
env.dispose();
}
@Test
public void testLinkFileUpdate() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("B")));
env.waitForTree();
// Add file 'x' while folder A and linked-folder B are both expanded
aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
//
// Verify good state after everything created
//
// /A
// x
// y -> x
// /B -> /A (linked-folder)
// x
// y -> x
//
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Remove 'x' file and verify broken links are reflected
xNode = (DomainFileNode) aFolderNode.getChild("x");
xNode.getDomainFile().delete();
env.waitForTree();
assertNull(aFolderNode.getChild("x"));
yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate1() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
env.waitForTree();
// Add file 'x' before folder A and is expanded and linked-folder B is not
aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
env.waitForTree();
//
// Verify good state after everything created
//
// /A
// x
// y -> x
// /B -> /A (linked-folder)
// x
// y -> x
//
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Remove 'x' file and verify broken links are reflected
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
xNode.getDomainFile().delete();
env.waitForTree();
assertNull(aFolderNode.getChild("x"));
yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate2() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
env.waitForTree();
// Add file 'x' while linked-folder B is expanded and folder A is not
DomainFile xFile = aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
//// Verify good state after everything created (leave A collapsed)
DomainFileNode xNode = (DomainFileNode) bFolderLinkNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
//// Remove 'x' file
xFile.delete();
env.waitForTree();
assertNull(bFolderLinkNode.getChild("x"));
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate3() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
FolderLinkContentHandler.INSTANCE);
DomainFolder usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
env.waitForTree();
// Add file 'bash'
DomainFile bashFile = usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
DomainFileNode binFolderLinkNode = (DomainFileNode) modelRoot.getChild("bin");
assertNotNull(binFolderLinkNode.getChild("bash"));
//
// /bin -> /usr/bin (linked folder)
// bash
// /usr
// /bin
// bash
//
// Delete real folders and content
bashFile.delete();
usrBinFolder.delete(); // /usr/bin
rootFolder.getFolder("usr").delete();
env.waitForTree();
assertNull(binFolderLinkNode.getChild("bash"));
waitForRefresh(binFolderLinkNode);
env.waitForTree();
String tip = binFolderLinkNode.getToolTip();
assertTrue(tip.contains("Broken"));
// binLinkFile.delete();
env.waitForTree();
// Re-create content
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
FolderLinkContentHandler.INSTANCE);
usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
env.waitForTree();
program = (Program) programAFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
assertNotNull(program);
usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
DomainFileNode xLinkedFileNode = (DomainFileNode) binFolderLinkNode.getChild("bash");
assertNotNull(xLinkedFileNode);
tip = binFolderLinkNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Repeat removal of folder A and its contents
bashFile = usrBinFolder.getFile("bash");
assertNotNull(bashFile);
bashFile.delete();
usrBinFolder.delete();
rootFolder.getFolder("usr").delete();
env.waitForTree();
assertNull(binFolderLinkNode.getChild("bash"));
waitForRefresh(binFolderLinkNode);
env.waitForTree();
tip = binFolderLinkNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
private void waitForRefresh(DomainFileNode fileNode) {
waitFor(new BooleanSupplier() {
@Override
public boolean getAsBoolean() {
return !fileNode.hasPendingRefresh();
}
});
}
}

View file

@ -32,6 +32,7 @@ import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.server.remote.ServerTestUtil; import ghidra.server.remote.ServerTestUtil;
import ghidra.test.*; import ghidra.test.*;
import ghidra.util.Swing;
import ghidra.util.exception.DuplicateFileException; import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
@ -50,13 +51,16 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
/** /**
/abc/ (folder) /abc/ (folder)
abc -> /xyz/abc (circular) abc -> /xyz/abc (circular folder allowed as internal)
foo (program file) foo (program file)
/xyz/ /xyz/
abc -> /abc (folder link) abc -> /abc (folder link)
abc -> (circular) abc -> /xyz/abc (circular folder allowed as internal)
foo foo
foo -> /abc/foo (program link) foo -> /abc/foo (program link)
/e -> f (circular folder link path)
/f -> g (circular folder link path)
/g -> e (circular folder link path)
**/ **/
DomainFolder rootFolder = env.getRootFolder(); DomainFolder rootFolder = env.getRootFolder();
@ -72,6 +76,18 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
programFile.copyToAsLink(xyzFolder, false); programFile.copyToAsLink(xyzFolder, false);
// Circular folder-link path without real folder
rootFolder.createLinkFile(rootFolder.getProjectData(), "/f", true, "e",
FolderLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/g", true, "f",
FolderLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/e", true, "g",
FolderLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(),
"/home/tsharr2/Examples/linktest/usr/lib64/../lib64", true, "nested2lib64",
FolderLinkContentHandler.INSTANCE);
env.waitForTree(); env.waitForTree();
} }
@ -257,23 +273,154 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
} }
@Test @Test
public void testBrokenFolderLink() throws Exception { public void testCircularFolderLink1() throws Exception {
// //
// Verify broken folder-link status for /abc/abc which has circular reference // Verify broken folder-link status for /e which has circular reference
//
DomainFileNode eLinkNode = waitForFileNode("/e");
assertTrue(eLinkNode.isFolderLink());
String displayName = runSwing(() -> eLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(eLinkNode.getDomainFile(), null));
String tooltip = eLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Verify broken folder-link status for /g which has circular reference
//
DomainFileNode gLinkNode = waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Verify broken folder-link status for /f which has circular reference
//
DomainFileNode fLinkNode = waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Rename folder /e to /ABC causing folder-links to have broken path
//
Swing.runNow(() -> eLinkNode.setName("ABC"));
env.waitForTree(); // give time for ChangeManager to update
// Verify /e node not found
assertNull(env.getRootNode().getChild("e"));
//
// Verify broken folder-link status for /ABC (final folder /e not found)
//
DomainFileNode abcLinkNode = waitForFileNode("/ABC");
assertTrue(abcLinkNode.isFolderLink());
displayName = runSwing(() -> abcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
tooltip = abcLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Verify broken folder-link status for /g (final folder /e not found)
//
waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Verify broken folder-link status for /f (final folder /e not found)
//
waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Create folder /e
//
DomainFolder rootFolder = env.getRootFolder();
rootFolder.createFolder("e");
env.waitForTree(); // give time for ChangeManager to update
//
// Verify good folder-link status for /ABC
//
waitForFileNode("/ABC");
assertTrue(abcLinkNode.isFolderLink());
displayName = runSwing(() -> abcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
//
// Verify good folder-link status for /g
//
waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
//
// Verify good folder-link status for /f
//
waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
}
@Test
public void testCircularFolderLink2() throws Exception {
//
// Verify good folder-link internal status for /abc/abc which has allowed circular reference
// //
DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc"); DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
assertTrue(abcAbcLinkNode.isFolderLink()); assertTrue(abcAbcLinkNode.isFolderLink());
String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText()); String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, assertTrue("Unexpected node display name: " + displayName,
displayName.endsWith(" /xyz/abc")); displayName.endsWith(" /xyz/abc"));
assertEquals(LinkStatus.BROKEN, assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null)); LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
String tooltip = abcAbcLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
// //
// Verify good folder-link internal status for /xyz/abc which has circular reference // Verify good folder-link internal status for /xyz/abc which has allowed circular reference
// //
DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc"); DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
assertTrue(xyzAbcLinkNode.isFolderLink()); assertTrue(xyzAbcLinkNode.isFolderLink());
@ -283,17 +430,15 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null)); LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
// //
// Verify broken folder-link status for /xyz/abc/abc which has circular reference // Verify good folder-link internal status for /xyz/abc/abc which has allowed circular reference
// //
DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc"); DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc");
assertTrue(abcLinkedNode.isFolderLink()); assertTrue(abcLinkedNode.isFolderLink());
displayName = runSwing(() -> abcLinkedNode.getDisplayText()); displayName = runSwing(() -> abcLinkedNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, assertTrue("Unexpected node display name: " + displayName,
displayName.endsWith(" /xyz/abc")); displayName.endsWith(" /xyz/abc"));
assertEquals(LinkStatus.BROKEN, assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null)); LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
tooltip = abcLinkedNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
// //
// Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken // Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken
@ -315,7 +460,7 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
displayName.endsWith(" /xyz/abc")); displayName.endsWith(" /xyz/abc"));
assertEquals(LinkStatus.BROKEN, assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null)); LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
tooltip = ABCAbcLinkNode.getToolTip().replace("&nbsp;", " "); String tooltip = ABCAbcLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /abc")); assertTrue(tooltip.contains("folder not found: /abc"));
env.waitForTree(); // give time for ChangeManager to update env.waitForTree(); // give time for ChangeManager to update