mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 01:39:21 +02:00
Merge remote-tracking branch 'origin/GP-5907-5908_ghidra1_ProjectTreeUpdateAndDelete--SQUASHED'
This commit is contained in:
commit
f6148f063a
38 changed files with 1988 additions and 515 deletions
|
@ -115,7 +115,9 @@
|
|||
"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
|
||||
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
|
||||
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>
|
||||
</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>
|
||||
|
||||
|
||||
|
@ -656,7 +678,8 @@
|
|||
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
|
||||
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>
|
||||
</TR>
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ public class ProgramLocator {
|
|||
try {
|
||||
// Attempt to resolve to actual linked-file to allow for
|
||||
// direct URL reference
|
||||
domainFile = linkedFile.getLinkedFile();
|
||||
domainFile = linkedFile.getRealFile();
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Failed to resolve linked-file", e);
|
||||
|
|
|
@ -18,14 +18,17 @@ package ghidra.app.util.task;
|
|||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.app.plugin.core.progmgr.ProgramLocator;
|
||||
import ghidra.app.util.dialog.CheckoutDialog;
|
||||
import ghidra.framework.client.ClientUtil;
|
||||
import ghidra.framework.client.RepositoryAdapter;
|
||||
import ghidra.framework.data.LinkHandler.LinkStatus;
|
||||
import ghidra.framework.main.AppInfo;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.LinkFileInfo;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURLQuery;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter;
|
||||
|
@ -142,6 +145,18 @@ public class ProgramOpener {
|
|||
}
|
||||
|
||||
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();
|
||||
performOptionalCheckout(domainFile, monitor);
|
||||
try {
|
||||
|
|
|
@ -24,7 +24,8 @@ import org.apache.logging.log4j.Logger;
|
|||
|
||||
import db.buffers.LocalManagedBufferFile;
|
||||
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.RepositoryManager;
|
||||
import ghidra.util.InvalidNameException;
|
||||
|
@ -282,11 +283,8 @@ public class RepositoryFolder {
|
|||
throw new DuplicateFileException(itemName + " already exists");
|
||||
}
|
||||
|
||||
LocalTextDataItem textDataItem = fileSystem.createTextDataItem(getPathname(), itemName,
|
||||
fileID, contentType, textData, null); // comment conveyed with Version info below
|
||||
|
||||
Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
|
||||
textDataItem.setVersionInfo(singleVersion);
|
||||
fileSystem.createTextDataItem(getPathname(), itemName, fileID, contentType, textData,
|
||||
comment, user);
|
||||
|
||||
RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
|
||||
fileMap.put(itemName, rf);
|
||||
|
|
|
@ -211,13 +211,14 @@ public interface FileSystem {
|
|||
* @param contentType application defined content type
|
||||
* @param textData text data (required)
|
||||
* @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
|
||||
* @throws DuplicateFileException Thrown if a folderItem with that name already exists.
|
||||
* @throws InvalidNameException if the name has illegal characters.
|
||||
* @throws IOException if an IO error occurs.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -344,23 +345,22 @@ public interface FileSystem {
|
|||
*/
|
||||
public static String normalizePath(String path) throws IllegalArgumentException {
|
||||
if (!path.startsWith(SEPARATOR)) {
|
||||
throw new IllegalArgumentException("Absolute path required");
|
||||
throw new IllegalArgumentException("Absolute path required: " + path);
|
||||
}
|
||||
|
||||
String[] split = path.split(SEPARATOR);
|
||||
|
||||
ArrayList<String> elements = new ArrayList<>();
|
||||
elements.add(SEPARATOR);
|
||||
for (int i = 1; i < split.length; i++) {
|
||||
String e = split[i];
|
||||
if (e.length() == 0) {
|
||||
throw new IllegalArgumentException("Invalid path with empty element: " + path);
|
||||
}
|
||||
if ("..".equals(e)) {
|
||||
try {
|
||||
// remove last element
|
||||
elements.removeLast();
|
||||
}
|
||||
catch (NoSuchElementException ex) {
|
||||
if (elements.size() == 0) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
}
|
||||
|
@ -369,6 +369,9 @@ public interface FileSystem {
|
|||
continue;
|
||||
}
|
||||
else {
|
||||
if (i < (split.length - 1)) {
|
||||
e += SEPARATOR;
|
||||
}
|
||||
elements.add(e);
|
||||
}
|
||||
}
|
||||
|
@ -379,9 +382,11 @@ public interface FileSystem {
|
|||
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for (String e : elements) {
|
||||
buf.append(SEPARATOR);
|
||||
buf.append(e);
|
||||
}
|
||||
if (path.endsWith(SEPARATOR)) {
|
||||
buf.append(SEPARATOR);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -556,7 +556,7 @@ public abstract class LocalFileSystem implements FileSystem {
|
|||
|
||||
@Override
|
||||
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 {
|
||||
|
||||
// comment is ignored
|
||||
|
@ -573,6 +573,12 @@ public abstract class LocalFileSystem implements FileSystem {
|
|||
try {
|
||||
ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
|
||||
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());
|
||||
}
|
||||
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 name child folder or item name
|
||||
* @return pathname
|
||||
*/
|
||||
protected final static String getPath(String parentPath, String name) {
|
||||
public final static String getPath(String parentPath, String name) {
|
||||
if (parentPath.length() == 1) {
|
||||
return parentPath + 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) {
|
||||
return null;
|
||||
}
|
||||
int index = path.lastIndexOf(SEPARATOR_CHAR);
|
||||
if (index == 0) {
|
||||
return SEPARATOR;
|
||||
}
|
||||
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) {
|
||||
return path;
|
||||
}
|
||||
|
|
|
@ -89,14 +89,14 @@ public abstract class LocalFolderItem implements FolderItem {
|
|||
* @param useDataDir if true the getDataDir() method must return an appropriate
|
||||
* directory for data storage.
|
||||
* @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,
|
||||
boolean create) throws IOException {
|
||||
this.fileSystem = fileSystem;
|
||||
this.propertyFile = propertyFile;
|
||||
this.isVersioned = fileSystem.isVersioned();
|
||||
this.useDataDir = useDataDir || isVersioned;
|
||||
this.useDataDir = useDataDir;
|
||||
|
||||
boolean success = false;
|
||||
try {
|
||||
|
|
|
@ -229,7 +229,7 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
|
|||
|
||||
@Override
|
||||
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 {
|
||||
repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment);
|
||||
return (TextDataItem) getItem(parentPath, name);
|
||||
|
|
|
@ -729,6 +729,10 @@ public class DefaultProjectData implements ProjectData {
|
|||
|
||||
@Override
|
||||
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 {
|
||||
rootFolderData.refresh(true, true, projectDisposalMonitor);
|
||||
}
|
||||
|
@ -1057,7 +1061,8 @@ public class DefaultProjectData implements ProjectData {
|
|||
@Override
|
||||
public void folderCreated(final String parentPath, final String name) {
|
||||
synchronized (fileSystem) {
|
||||
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, true);
|
||||
boolean lazy = !rootFolderData.mustVisit(parentPath);
|
||||
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, lazy);
|
||||
if (folderData != null) {
|
||||
try {
|
||||
folderData.folderChanged(name);
|
||||
|
@ -1111,7 +1116,9 @@ public class DefaultProjectData implements ProjectData {
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
folderData = rootFolderData.getFolderPathData(newParentPath, true);
|
||||
|
||||
boolean lazy = !rootFolderData.mustVisit(newParentPath);
|
||||
folderData = rootFolderData.getFolderPathData(newParentPath, lazy);
|
||||
if (folderData != null) {
|
||||
try {
|
||||
folderData.folderChanged(folderName);
|
||||
|
@ -1338,7 +1345,7 @@ public class DefaultProjectData implements ProjectData {
|
|||
}
|
||||
}
|
||||
|
||||
GhidraFolderData getRootFolderData() {
|
||||
RootGhidraFolderData getRootFolderData() {
|
||||
return rootFolderData;
|
||||
}
|
||||
|
||||
|
|
|
@ -165,6 +165,16 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFolderLink() {
|
||||
try {
|
||||
return getFileData().isFolderLink();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkFileInfo getLinkInfo() {
|
||||
return isLink() ? this : null;
|
||||
|
@ -642,7 +652,7 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
|
|||
catch (IOException e) {
|
||||
fileError(e);
|
||||
}
|
||||
return new HashMap<>();
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
void fileChanged() {
|
||||
|
|
|
@ -81,7 +81,12 @@ public class GhidraFileData {
|
|||
private GhidraFolderData parent;
|
||||
private String name;
|
||||
private String fileID;
|
||||
private String contentType;
|
||||
|
||||
private String linkPath;
|
||||
private String absoluteLinkPath;
|
||||
private boolean isLink;
|
||||
private boolean isFolderLink;
|
||||
|
||||
private LocalFolderItem folderItem;
|
||||
private FolderItem versionedFolderItem;
|
||||
|
@ -98,7 +103,10 @@ public class GhidraFileData {
|
|||
// 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 name file name
|
||||
* @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
|
||||
* 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 name file name
|
||||
* @param folderItem local folder item
|
||||
|
@ -138,10 +148,15 @@ public class GhidraFileData {
|
|||
|
||||
validateCheckout();
|
||||
updateFileID();
|
||||
|
||||
registerLinkFile();
|
||||
}
|
||||
|
||||
void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) {
|
||||
linkPath = null;
|
||||
|
||||
unregisterLinkFile();
|
||||
|
||||
contentType = null;
|
||||
icon = null;
|
||||
disabledIcon = null;
|
||||
|
||||
|
@ -151,6 +166,8 @@ public class GhidraFileData {
|
|||
validateCheckout();
|
||||
boolean fileIDset = updateFileID();
|
||||
|
||||
registerLinkFile();
|
||||
|
||||
if (parent.visited()) {
|
||||
// NOTE: we should maintain some cached data so we can determine if something really changed
|
||||
listener.domainFileStatusChanged(getDomainFile(), fileIDset);
|
||||
|
@ -158,7 +175,13 @@ public class GhidraFileData {
|
|||
}
|
||||
|
||||
private boolean refresh() throws IOException {
|
||||
linkPath = null;
|
||||
|
||||
unregisterLinkFile();
|
||||
|
||||
contentType = null;
|
||||
icon = null;
|
||||
disabledIcon = null;
|
||||
|
||||
String parentPath = parent.getPathname();
|
||||
if (folderItem == null) {
|
||||
folderItem = fileSystem.getItem(parentPath, name);
|
||||
|
@ -183,7 +206,54 @@ public class GhidraFileData {
|
|||
if (folderItem == null && versionedFolderItem == null) {
|
||||
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() {
|
||||
|
@ -204,8 +274,6 @@ public class GhidraFileData {
|
|||
if (mergeInProgress) {
|
||||
return;
|
||||
}
|
||||
icon = null;
|
||||
disabledIcon = null;
|
||||
fileIDset |= refresh();
|
||||
if (parent.visited()) {
|
||||
// NOTE: we should maintain some cached data so we can determine if something really changed
|
||||
|
@ -267,6 +335,7 @@ public class GhidraFileData {
|
|||
*/
|
||||
void dispose() {
|
||||
projectData.removeFromIndex(fileID);
|
||||
unregisterLinkFile();
|
||||
// NOTE: clearing the following can cause issues since there may be some residual
|
||||
// activity/use which will get a NPE
|
||||
// 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
|
||||
* @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
|
||||
|
@ -445,14 +510,22 @@ public class GhidraFileData {
|
|||
*/
|
||||
String getContentType() {
|
||||
synchronized (fileSystem) {
|
||||
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
|
||||
// this can happen when we are trying to load a version file from
|
||||
// a server to which we are not connected
|
||||
if (item == null) {
|
||||
return ContentHandler.MISSING_CONTENT;
|
||||
if (contentType != null) {
|
||||
return contentType;
|
||||
}
|
||||
String contentType = item.getContentType();
|
||||
return contentType != null ? contentType : ContentHandler.UNKNOWN_CONTENT;
|
||||
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
|
||||
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) {
|
||||
versionedFileSystem.createTextDataItem(parentPath, name,
|
||||
folderItem.getFileID(), folderItem.getContentType(),
|
||||
textDataItem.getTextData(), comment);
|
||||
textDataItem.getTextData(), comment, user);
|
||||
}
|
||||
else {
|
||||
throw new IOException(
|
||||
|
@ -2237,17 +2310,26 @@ public class GhidraFileData {
|
|||
* @return true if link file else false for a normal domain file
|
||||
*/
|
||||
boolean isLink() {
|
||||
try {
|
||||
return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
return isLink; // relies on refresh to initialize
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* @return associated link path or null if not a link file
|
||||
|
@ -2275,12 +2357,19 @@ public class GhidraFileData {
|
|||
return linkPath;
|
||||
}
|
||||
|
||||
String path = 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 {
|
||||
|
@ -2292,13 +2381,7 @@ public class GhidraFileData {
|
|||
}
|
||||
absPath += path;
|
||||
}
|
||||
try {
|
||||
absPath = FileSystem.normalizePath(absPath);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new IOException("Invalid link path: " + linkPath);
|
||||
}
|
||||
return absPath;
|
||||
return FileSystem.normalizePath(absPath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2396,7 +2479,7 @@ public class GhidraFileData {
|
|||
if (!StringUtils.isBlank(lp)) {
|
||||
newParent.getLocalFileSystem()
|
||||
.createTextDataItem(pathname, targetName,
|
||||
FileIDFactory.createFileID(), contentType, lp, null);
|
||||
FileIDFactory.createFileID(), contentType, lp, null, null);
|
||||
}
|
||||
else {
|
||||
throw new IOException(
|
||||
|
|
|
@ -64,7 +64,7 @@ class GhidraFolderData {
|
|||
// folderList and fileList are only be used if visited is true
|
||||
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, GhidraFolderData> folderDataCache = new HashMap<>();
|
||||
|
@ -122,6 +122,15 @@ class GhidraFolderData {
|
|||
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
|
||||
*/
|
||||
|
@ -283,7 +292,7 @@ class GhidraFolderData {
|
|||
parent.folderList.remove(oldName);
|
||||
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());
|
||||
|
||||
listener.domainFolderRenamed(newFolder, oldName);
|
||||
|
@ -385,9 +394,7 @@ class GhidraFolderData {
|
|||
* @param newFileName file name after rename
|
||||
*/
|
||||
void fileRenamed(String oldFileName, String newFileName) {
|
||||
GhidraFileData fileData;
|
||||
synchronized (fileSystem) {
|
||||
fileData = fileDataCache.remove(oldFileName);
|
||||
GhidraFileData fileData = fileDataCache.remove(oldFileName);
|
||||
if (fileData == null || this != fileData.getParent() ||
|
||||
!newFileName.equals(fileData.getName())) {
|
||||
throw new AssertException();
|
||||
|
@ -397,7 +404,6 @@ class GhidraFolderData {
|
|||
listener.domainFileRenamed(getDomainFile(newFileName), oldFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update file list/cache based upon change of parent for a file.
|
||||
|
@ -510,8 +516,7 @@ class GhidraFolderData {
|
|||
* if this folder has been visited
|
||||
* @param folderName name of folder which was removed
|
||||
*/
|
||||
void folderRemoved(String folderName) {
|
||||
synchronized (fileSystem) {
|
||||
private void folderRemoved(String folderName) {
|
||||
GhidraFolderData folderData = folderDataCache.remove(folderName);
|
||||
if (folderData != null) {
|
||||
folderData.dispose();
|
||||
|
@ -520,7 +525,6 @@ class GhidraFolderData {
|
|||
listener.domainFolderRemoved(getDomainFolder(), folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
// FIXME: inconsistent use of forced-recursive refresh and cached folderList
|
||||
|
||||
String path = getPathname();
|
||||
HashSet<String> newSet = new HashSet<>();
|
||||
|
||||
|
@ -772,6 +778,10 @@ class GhidraFolderData {
|
|||
void refresh(boolean recursive, boolean force, TaskMonitor monitor) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
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");
|
||||
}
|
||||
if (monitor != null && monitor.isCancelled()) {
|
||||
|
@ -780,6 +790,9 @@ class GhidraFolderData {
|
|||
if (visited && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited = true;
|
||||
|
||||
try {
|
||||
updateExistenceState();
|
||||
}
|
||||
|
@ -797,7 +810,8 @@ class GhidraFolderData {
|
|||
throw new FileNotFoundException("Folder not found: " + getPathname());
|
||||
}
|
||||
|
||||
try {
|
||||
// FIXME: If forced we should be refreshing folder/file lists
|
||||
|
||||
refreshFiles(monitor);
|
||||
|
||||
if (monitor != null && monitor.isCancelled()) {
|
||||
|
@ -806,10 +820,6 @@ class GhidraFolderData {
|
|||
|
||||
refreshFolders(recursive, monitor);
|
||||
}
|
||||
finally {
|
||||
visited = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -843,8 +853,11 @@ class GhidraFolderData {
|
|||
try {
|
||||
folderData = new GhidraFolderData(this, folderName);
|
||||
folderDataCache.put(folderName, folderData);
|
||||
if (folderData.mustVisit()) {
|
||||
folderData.refresh(false, true, TaskMonitor.DUMMY);
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -1226,6 +1239,10 @@ class GhidraFolderData {
|
|||
GhidraFolder newFolder = getDomainFolder();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1398,17 +1415,34 @@ class GhidraFolderData {
|
|||
String linkFilename, LinkHandler<?> lh) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
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)) {
|
||||
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;
|
||||
if (sourceProjectData == projectData) {
|
||||
if (referenceMyProject) {
|
||||
if (makeRelative) {
|
||||
linkPath = getRelativePath(pathname, getPathname());
|
||||
linkPath = getRelativePath(pathname, getPathname(), isFolderLink);
|
||||
}
|
||||
else {
|
||||
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 relativePath = linkParentPath.relativize(referencedPath);
|
||||
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 += FileSystem.SEPARATOR;
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
String linkFilename) throws IOException, InvalidNameException {
|
||||
|
||||
fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(),
|
||||
getContentType(), linkPath, null);
|
||||
getContentType(), linkPath, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -334,9 +334,20 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
}
|
||||
|
||||
String path = linkPath;
|
||||
boolean isRelative = false;
|
||||
if (!GhidraURL.isGhidraURL(path)) {
|
||||
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)) {
|
||||
path += FileSystem.SEPARATOR;
|
||||
}
|
||||
|
@ -346,7 +357,11 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
return FileSystem.normalizePath(path);
|
||||
}
|
||||
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;
|
||||
|
@ -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
|
||||
* 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 linkPath internal linkPath
|
||||
* @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
|
||||
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
||||
linkPath += FileSystem.SEPARATOR;
|
||||
|
@ -418,7 +438,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
if (parent instanceof LinkedDomainFolder lf) {
|
||||
try {
|
||||
projectData = lf.getLinkedProjectData();
|
||||
addLinkPathPath(linkPathsVisited, lf.getLinkedPathname());
|
||||
addLinkPath(linkPathsVisited, lf.getLinkedPathname());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Unexpected", e);
|
||||
|
@ -426,7 +446,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
}
|
||||
else {
|
||||
projectData = parent.getProjectData();
|
||||
addLinkPathPath(linkPathsVisited, file.getPathname());
|
||||
addLinkPath(linkPathsVisited, file.getPathname());
|
||||
}
|
||||
|
||||
String contentType = file.getContentType();
|
||||
|
@ -466,19 +486,22 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
return nextLinkFile;
|
||||
}
|
||||
|
||||
if (!addLinkPathPath(linkPathsVisited, linkPath)) {
|
||||
errorConsumer.accept("Link has a circular reference");
|
||||
break; // broken and can't continue
|
||||
}
|
||||
|
||||
DomainFile linkedFile = null;
|
||||
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
||||
linkedFile = projectData.getFile(linkPath);
|
||||
}
|
||||
|
||||
DomainFolder linkedFolder = null;
|
||||
if (isFolderLink) {
|
||||
// Check for folder existence at linkPath
|
||||
if (getNonLinkedFolder(projectData, linkPath) != null) {
|
||||
linkedFolder = getNonLinkedFolder(projectData, linkPath);
|
||||
}
|
||||
|
||||
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
|
||||
if (linkedFile != null) {
|
||||
LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo();
|
||||
|
@ -492,7 +515,6 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
|||
statusConsumer.accept(LinkStatus.INTERNAL);
|
||||
return nextLinkFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (linkedFile == null) {
|
||||
String acceptableType = isFolderLink ? "folder" : "file";
|
||||
|
|
|
@ -43,21 +43,21 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
private final LinkedGhidraSubFolder parent;
|
||||
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.fileName = fileName;
|
||||
this.fileName = realDomainFile.getName();
|
||||
this.realDomainFile = realDomainFile;
|
||||
this.linkInfo = realDomainFile.isLink() ? new LinkedFileLinkInfo() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile getLinkedFile() throws IOException {
|
||||
public DomainFile getRealFile() throws IOException {
|
||||
return parent.getLinkedFile(fileName);
|
||||
}
|
||||
|
||||
private DomainFile getLinkedFileNoError() {
|
||||
return parent.getLinkedFileNoError(fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFolder getParent() {
|
||||
return parent;
|
||||
|
@ -94,18 +94,18 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return getLinkedFileNoError() != null;
|
||||
DomainFile df = parent.getLinkedFileNoError(fileName);
|
||||
return df != null && df.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileID() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getFileID() : null;
|
||||
return realDomainFile.getFileID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile setName(String newName) throws InvalidNameException, IOException {
|
||||
String name = getLinkedFile().setName(newName).getName();
|
||||
String name = getRealFile().setName(newName).getName();
|
||||
return parent.getFile(name);
|
||||
}
|
||||
|
||||
|
@ -156,46 +156,40 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT;
|
||||
return realDomainFile.getContentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getDomainObjectClass() : DomainObject.class;
|
||||
return realDomainFile.getDomainObjectClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
|
||||
return getLinkedFile().getChangesByOthersSinceCheckout();
|
||||
return getRealFile().getChangesByOthersSinceCheckout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
|
||||
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
|
||||
return getLinkedFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
|
||||
return getRealFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObject getOpenedDomainObject(Object consumer) {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
if (df != null) {
|
||||
return df.getOpenedDomainObject(consumer);
|
||||
}
|
||||
return null;
|
||||
return realDomainFile.getOpenedDomainObject(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
|
||||
throws VersionException, IOException, CancelledException {
|
||||
return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor);
|
||||
return getRealFile().getReadOnlyDomainObject(consumer, version, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
|
||||
throws VersionException, IOException, CancelledException {
|
||||
return getLinkedFile().getImmutableDomainObject(consumer, version, monitor);
|
||||
return getRealFile().getImmutableDomainObject(consumer, version, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -226,191 +220,174 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public long getLastModifiedTime() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getLastModifiedTime() : 0;
|
||||
return realDomainFile.getLastModifiedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean disabled) {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON;
|
||||
return realDomainFile.getIcon(disabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCheckedOut() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isCheckedOut() : false;
|
||||
return realDomainFile.isCheckedOut();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCheckedOutExclusive() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isCheckedOutExclusive() : false;
|
||||
return realDomainFile.isCheckedOutExclusive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean modifiedSinceCheckout() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.modifiedSinceCheckout() : false;
|
||||
return realDomainFile.modifiedSinceCheckout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCheckout() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.canCheckout() : false;
|
||||
return realDomainFile.canCheckout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCheckin() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.canCheckin() : false;
|
||||
return realDomainFile.canCheckin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canMerge() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.canMerge() : false;
|
||||
return realDomainFile.canMerge();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAddToRepository() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.canAddToRepository() : false;
|
||||
return realDomainFile.canAddToRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadOnly(boolean state) throws IOException {
|
||||
getLinkedFile().setReadOnly(state);
|
||||
getRealFile().setReadOnly(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
// read-only state not reflected by icon
|
||||
return df != null ? df.isReadOnly() : true;
|
||||
return realDomainFile.isReadOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersioned() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isVersioned() : false;
|
||||
return realDomainFile.isVersioned();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHijacked() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isHijacked() : false;
|
||||
return realDomainFile.isHijacked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLatestVersion() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION;
|
||||
return realDomainFile.getLatestVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLatestVersion() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isLatestVersion() : true;
|
||||
return realDomainFile.isLatestVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getVersion() : DomainFile.DEFAULT_VERSION;
|
||||
return realDomainFile.getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version[] getVersionHistory() throws IOException {
|
||||
DomainFile df = getLinkedFile();
|
||||
DomainFile df = getRealFile();
|
||||
return df != null ? df.getVersionHistory() : new Version[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
getLinkedFile().addToVersionControl(comment, keepCheckedOut, monitor);
|
||||
getRealFile().addToVersionControl(comment, keepCheckedOut, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkout(boolean exclusive, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
return getLinkedFile().checkout(exclusive, monitor);
|
||||
return getRealFile().checkout(exclusive, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor)
|
||||
throws IOException, VersionException, CancelledException {
|
||||
getLinkedFile().checkin(checkinHandler, monitor);
|
||||
getRealFile().checkin(checkinHandler, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void merge(boolean okToUpgrade, TaskMonitor monitor)
|
||||
throws IOException, VersionException, CancelledException {
|
||||
getLinkedFile().merge(okToUpgrade, monitor);
|
||||
getRealFile().merge(okToUpgrade, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undoCheckout(boolean keep) throws IOException {
|
||||
getLinkedFile().undoCheckout(keep);
|
||||
getRealFile().undoCheckout(keep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undoCheckout(boolean keep, boolean force) throws IOException {
|
||||
getLinkedFile().undoCheckout(keep, force);
|
||||
getRealFile().undoCheckout(keep, force);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateCheckout(long checkoutId) throws IOException {
|
||||
getLinkedFile().terminateCheckout(checkoutId);
|
||||
getRealFile().terminateCheckout(checkoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemCheckoutStatus[] getCheckouts() throws IOException {
|
||||
return getLinkedFile().getCheckouts();
|
||||
return getRealFile().getCheckouts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemCheckoutStatus getCheckoutStatus() throws IOException {
|
||||
return getLinkedFile().getCheckoutStatus();
|
||||
return getRealFile().getCheckoutStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() throws IOException {
|
||||
getLinkedFile().delete();
|
||||
getRealFile().delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(int version) throws IOException {
|
||||
getLinkedFile().delete(version);
|
||||
getRealFile().delete(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile moveTo(DomainFolder newParent) throws IOException {
|
||||
return getLinkedFile().moveTo(newParent);
|
||||
return getRealFile().moveTo(newParent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
return getLinkedFile().copyTo(newParent, monitor);
|
||||
return getRealFile().copyTo(newParent, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
return getLinkedFile().copyVersionTo(version, destFolder, monitor);
|
||||
return getRealFile().copyVersionTo(version, destFolder, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
|
||||
return getLinkedFile().copyToAsLink(newParent, relative);
|
||||
return getRealFile().copyToAsLink(newParent, relative);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLinkingSupported() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isLinkingSupported() : false;
|
||||
return realDomainFile.isLinkingSupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -420,8 +397,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public boolean isChanged() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isChanged() : false;
|
||||
return realDomainFile.isChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -436,31 +412,57 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
|
||||
getLinkedFile().packFile(file, monitor);
|
||||
getRealFile().packFile(file, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getMetadata() : Map.of();
|
||||
return realDomainFile.getMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() throws IOException {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.length() : 0;
|
||||
return realDomainFile.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLink() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.isLink() : false;
|
||||
return linkInfo != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkFileInfo getLinkInfo() {
|
||||
DomainFile df = getLinkedFileNoError();
|
||||
return df != null ? df.getLinkInfo() : null;
|
||||
return linkInfo;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -470,12 +472,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
String str = parent.toString();
|
||||
if (!str.endsWith("/")) {
|
||||
str += "/";
|
||||
}
|
||||
str += getName();
|
||||
return str;
|
||||
return getPathname() + "->" + realDomainFile.getPathname();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
|
||||
|
@ -87,7 +88,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
|||
* {@link #getProjectData() project data} instance.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
|
||||
|
@ -114,7 +116,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
|||
if (!(obj instanceof LinkedGhidraFolder other)) {
|
||||
return false;
|
||||
}
|
||||
return linkedPathname.equals(other.linkedPathname) &&
|
||||
return getPathname().equals(other.getPathname()) &&
|
||||
folderLinkFile.equals(other.folderLinkFile);
|
||||
}
|
||||
|
||||
|
@ -238,9 +240,9 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
|||
@Override
|
||||
public String toString() {
|
||||
if (linkedFolder != null) {
|
||||
return "->" + getLinkedPathname();
|
||||
return getPathname() + "->" + getLinkedPathname();
|
||||
}
|
||||
return "->" + linkedFolderUrl.toString();
|
||||
return getPathname() + "->" + linkedFolderUrl.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -274,7 +274,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
|||
DomainFile[] files = linkedFolder.getFiles();
|
||||
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
|
||||
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;
|
||||
}
|
||||
|
@ -286,6 +286,10 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* @return file or null if not found or error occurs
|
||||
*/
|
||||
|
@ -300,6 +304,16 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
|||
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 {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
DomainFile df = linkedFolder.getFile(name);
|
||||
|
@ -311,8 +325,8 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
|||
|
||||
@Override
|
||||
public DomainFile getFile(String name) {
|
||||
DomainFile f = getLinkedFileNoError(name);
|
||||
return f != null ? new LinkedGhidraFile(this, name) : null;
|
||||
DomainFile file = getLinkedFileNoError(name);
|
||||
return file != null ? new LinkedGhidraFile(this, file) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -333,36 +347,40 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
|||
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
return linkedFolder.createFile(name, obj, monitor);
|
||||
DomainFile file = linkedFolder.createFile(name, obj, monitor);
|
||||
return getFile(file.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
return linkedFolder.createFile(name, packFile, monitor);
|
||||
DomainFile file = linkedFolder.createFile(name, packFile, monitor);
|
||||
return getFile(file.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
|
||||
boolean makeRelative, String linkFilename, LinkHandler<?> lh) throws IOException {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename,
|
||||
lh);
|
||||
DomainFile file = linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative,
|
||||
linkFilename, lh);
|
||||
return getFile(file.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh)
|
||||
throws IOException {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
|
||||
DomainFile file = linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
|
||||
return getFile(file.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
|
||||
DomainFolder linkedFolder = getRealFolder();
|
||||
DomainFolder child = linkedFolder.createFolder(name);
|
||||
return new LinkedGhidraSubFolder(parent, child.getName());
|
||||
DomainFolder folder = linkedFolder.createFolder(name);
|
||||
return getFolder(folder.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,11 +15,26 @@
|
|||
*/
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
import ghidra.framework.model.DomainFolderChangeListener;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
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) {
|
||||
super(projectData, listener);
|
||||
}
|
||||
|
@ -50,4 +65,97 @@ public class RootGhidraFolderData extends GhidraFolderData {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
|
|||
import ghidra.framework.preferences.Preferences;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.remote.User;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.filechooser.GhidraFileChooserModel;
|
||||
import ghidra.util.filechooser.GhidraFileFilter;
|
||||
|
@ -137,6 +138,7 @@ public class FrontEndPlugin extends Plugin
|
|||
private ProjectDataRenameAction renameAction;
|
||||
private ProjectDataOpenDefaultToolAction openAction;
|
||||
private ProjectDataFollowLinkAction followLinkAction;
|
||||
private ProjectDataSelectRealFileOrFolderAction selectRealFileOrFolderAction;
|
||||
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
|
||||
private ProjectDataCollapseAction<FrontEndProjectTreeContext> collapseAction;
|
||||
private ProjectDataSelectAction selectAction;
|
||||
|
@ -221,6 +223,7 @@ public class FrontEndPlugin extends Plugin
|
|||
// Top of popup menu actions - no group
|
||||
openAction = new ProjectDataOpenDefaultToolAction(owner, null);
|
||||
followLinkAction = new ProjectDataFollowLinkAction(this, null);
|
||||
selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
|
||||
|
||||
String groupName = "Cut/copy/paste/new1";
|
||||
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
|
||||
|
@ -258,6 +261,7 @@ public class FrontEndPlugin extends Plugin
|
|||
tool.addAction(deleteAction);
|
||||
tool.addAction(openAction);
|
||||
tool.addAction(followLinkAction);
|
||||
tool.addAction(selectRealFileOrFolderAction);
|
||||
tool.addAction(renameAction);
|
||||
tool.addAction(expandAction);
|
||||
tool.addAction(collapseAction);
|
||||
|
@ -1117,11 +1121,17 @@ public class FrontEndPlugin extends Plugin
|
|||
showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
|
||||
}
|
||||
else if (!dataTreePanel.isShowing()) {
|
||||
// Filter table on absolute link path
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -27,6 +27,9 @@ import docking.widgets.tree.GTreeNode;
|
|||
import ghidra.framework.data.LinkHandler;
|
||||
import ghidra.framework.main.datatree.DataTreeNode.NodeType;
|
||||
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
|
||||
|
@ -49,6 +52,10 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
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) {
|
||||
this.treePanel = treePanel;
|
||||
projectData = treePanel.getProjectData();
|
||||
|
@ -75,11 +82,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFileAdded(DomainFile file) {
|
||||
|
||||
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
||||
String fileName = file.getName();
|
||||
DomainFolder parentFolder = file.getParent();
|
||||
updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink),
|
||||
updateLinkedContent(parentFolder.getPathname(), p -> addFileNode(p, fileName, isFolderLink),
|
||||
ltn -> ltn.refreshLinks(fileName));
|
||||
|
||||
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
||||
if (folderNode != null && folderNode.isLoaded()) {
|
||||
addFileNode(folderNode, fileName, isFolderLink);
|
||||
|
@ -88,7 +97,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
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);
|
||||
if (folderNode != null) {
|
||||
updateChildren(folderNode);
|
||||
|
@ -97,14 +108,16 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFileRenamed(DomainFile file, String oldName) {
|
||||
|
||||
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
||||
updateLinkedContent(file.getParent(), p -> {
|
||||
updateLinkedContent(file.getParent().getPathname(), p -> {
|
||||
updateChildren(p);
|
||||
addFileNode(p, file.getName(), isFolderLink);
|
||||
}, ltn -> {
|
||||
ltn.refreshLinks(oldName);
|
||||
ltn.refreshLinks(file.getName());
|
||||
});
|
||||
|
||||
DomainFolder parent = file.getParent();
|
||||
skipLinkUpdate = true;
|
||||
try {
|
||||
|
@ -124,10 +137,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
|
||||
|
||||
LinkFileInfo linkInfo = file.getLinkInfo();
|
||||
boolean isFolderLink = linkInfo != null && linkInfo.isFolderLink();
|
||||
|
||||
DomainFolder parentFolder = file.getParent();
|
||||
updateLinkedContent(parentFolder, fn -> {
|
||||
/* No folder update required */
|
||||
updateLinkedContent(parentFolder.getPathname(), fn -> {
|
||||
// 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()));
|
||||
|
||||
DomainFileNode fileNode = findDomainFileNode(file, true);
|
||||
if (fileNode != null) {
|
||||
fileNode.refresh();
|
||||
|
@ -141,10 +166,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFolderAdded(DomainFolder folder) {
|
||||
|
||||
String folderName = folder.getName();
|
||||
DomainFolder parentFolder = folder.getParent();
|
||||
updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName),
|
||||
updateLinkedContent(parentFolder.getPathname(), p -> addFolderNode(p, folderName),
|
||||
ltn -> ltn.refreshLinks(folderName));
|
||||
|
||||
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
||||
if (folderNode != null && folderNode.isLoaded()) {
|
||||
addFolderNode(folderNode, folderName);
|
||||
|
@ -153,7 +180,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
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);
|
||||
if (folderNode != null) {
|
||||
updateChildren(folderNode);
|
||||
|
@ -162,17 +191,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFolderRenamed(DomainFolder folder, String oldName) {
|
||||
updateLinkedContent(folder.getParent(), p -> {
|
||||
updateChildren(p);
|
||||
addFolderNode(p, folder.getName());
|
||||
}, ltn -> {
|
||||
ltn.refreshLinks(oldName);
|
||||
ltn.refreshLinks(folder.getName());
|
||||
});
|
||||
DomainFolder parent = folder.getParent();
|
||||
|
||||
domainFolderMoved(folder.getParent().getPathname(), oldName, folder);
|
||||
|
||||
skipLinkUpdate = true;
|
||||
try {
|
||||
domainFolderRemoved(parent, oldName);
|
||||
domainFolderRemoved(folder.getParent(), oldName);
|
||||
domainFolderAdded(folder);
|
||||
}
|
||||
finally {
|
||||
|
@ -182,9 +206,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
@Override
|
||||
public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) {
|
||||
|
||||
domainFolderMoved(oldParent.getPathname(), folder.getName(), folder);
|
||||
|
||||
skipLinkUpdate = true;
|
||||
try {
|
||||
domainFolderRemoved(oldParent, folder.getName());
|
||||
domainFolderAdded(folder);
|
||||
}
|
||||
finally {
|
||||
skipLinkUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
//
|
||||
|
@ -210,9 +273,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
}
|
||||
|
||||
private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
|
||||
|
||||
Msg.debug(this, "addFileNode: " + node.getPathname() + " " + fileName + " " +
|
||||
Boolean.toString(isFolderLink));
|
||||
if (node.isLeaf() || !node.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for existance of file by that name
|
||||
DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
|
||||
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
||||
|
@ -234,6 +301,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
if (node.isLeaf() || !node.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for existance of folder by that name
|
||||
if (node.getChild(folderName, NodeType.FOLDER) != null) {
|
||||
return;
|
||||
|
@ -262,6 +330,20 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
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) {
|
||||
DomainFolderNode folderNode = root;
|
||||
for (String name : folderPath) {
|
||||
|
@ -284,6 +366,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
if (lazy && !folderNode.isLoaded()) {
|
||||
return null; // not visited
|
||||
}
|
||||
|
||||
boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
|
||||
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
|
||||
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
||||
|
@ -334,40 +417,51 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
@Override
|
||||
public void treeStructureChanged(TreeModelEvent e) {
|
||||
|
||||
// This is used when an existing node is loaded to register all of its link-file children
|
||||
// since the occurance of treeNodesChanged cannot be relied upon for notification of
|
||||
// these existing children.
|
||||
// NOTE: We have seen getTreePath return null in the test environment
|
||||
// immediately before ChangeManager disposal
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
if (!dataTreeNode.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
// Register all visible link-file nodes
|
||||
for (GTreeNode child : dataTreeNode.getChildren()) {
|
||||
if (child instanceof DomainFileNode fileNode) {
|
||||
if (fileNode.getDomainFile().isLink()) {
|
||||
|
||||
if (treeNode instanceof DomainFileNode fileNode) {
|
||||
addLinkFile(fileNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Not sure we need the following code
|
||||
// if (processLoadedChildren && dataTreeNode.isLoaded()) {
|
||||
// for (GTreeNode node : dataTreeNode.getChildren()) {
|
||||
// treeNodeChanged(node, true);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void treeNodesChanged(TreeModelEvent e) {
|
||||
|
||||
// This is used to register link-file nodes which may be added to the tree as a result
|
||||
// of changes to the associated project data.
|
||||
|
||||
Object treeNode = e.getTreePath().getLastPathComponent();
|
||||
if (treeNode instanceof DomainFileNode fileNode) {
|
||||
addLinkFile(fileNode);
|
||||
Object[] changedChildren = e.getChildren();
|
||||
if (changedChildren != null) {
|
||||
for (Object child : changedChildren) {
|
||||
treeNodeChanged(child, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
treeNodeChanged(e.getTreePath().getLastPathComponent(), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,6 +479,19 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
// 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
|
||||
* 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 {
|
||||
|
||||
String linkPath = LinkHandler.getAbsoluteLinkPath(file);
|
||||
if (linkPath == null) {
|
||||
return;
|
||||
|
@ -420,6 +528,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
if (isFolderLink) {
|
||||
folderLinkNode.addLinkedFolder(domainFileNode);
|
||||
addLoadedChildren(domainFileNode);
|
||||
}
|
||||
else {
|
||||
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
|
||||
* {@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
|
||||
* 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.
|
||||
* @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
|
||||
* is found which corresponds to the specified {@code parentFolder}.
|
||||
*/
|
||||
void updateLinkedContent(DomainFolder parentFolder, Consumer<DataTreeNode> folderNodeConsumer,
|
||||
Consumer<LinkedTreeNode> linkNodeConsumer) {
|
||||
private void updateLinkedContent(String parentFolderPath,
|
||||
Consumer<DataTreeNode> folderNodeConsumer, Consumer<LinkedTreeNode> linkNodeConsumer) {
|
||||
|
||||
if (!Swing.isSwingThread()) {
|
||||
throw new RuntimeException(
|
||||
"Listener and all node updates must operate in Swing thread");
|
||||
}
|
||||
|
||||
if (skipLinkUpdate) {
|
||||
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;
|
||||
folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
|
||||
for (int i = 1; i < pathElements.length; i++) {
|
||||
|
@ -469,9 +593,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
linkNodeConsumer.accept(folderLinkNode);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (clearRefreshedTrackingSet) {
|
||||
refreshedTrackingSet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 String name;
|
||||
|
||||
|
@ -491,7 +624,14 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
|
||||
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()) {
|
||||
continue;
|
||||
|
@ -529,6 +669,25 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
}
|
||||
|
||||
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
|
||||
// referencing another folder-link file and not the final referenced folder.
|
||||
if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
|
||||
|
@ -537,10 +696,28 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
}
|
||||
|
||||
private boolean refreshFolderLinks(String folderName) {
|
||||
|
||||
LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
|
||||
if (linkedTreeNode != null) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -557,12 +734,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
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) {
|
||||
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) {
|
||||
|
@ -579,23 +766,27 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
|||
}
|
||||
}
|
||||
|
||||
private static void refresh(Set<DomainFileNode> linkFiles) {
|
||||
List<DomainFileNode> purgeList = null;
|
||||
for (DomainFileNode fileLink : linkFiles) {
|
||||
DomainFile file = fileLink.getDomainFile();
|
||||
// Perform lazy purge of missing link files
|
||||
if (!file.isLink()) {
|
||||
if (purgeList == null) {
|
||||
purgeList = new ArrayList<>();
|
||||
}
|
||||
purgeList.add(fileLink);
|
||||
private void refresh(Set<DomainFileNode> linkFiles) {
|
||||
Iterator<DomainFileNode> linkFileIter = linkFiles.iterator();
|
||||
while (linkFileIter.hasNext()) {
|
||||
DomainFileNode fileLink = linkFileIter.next();
|
||||
if (fileLink.getParent() == null || !fileLink.getDomainFile().isLink()) {
|
||||
linkFileIter.remove();
|
||||
}
|
||||
else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -142,6 +142,33 @@ public class DataTree extends GTree {
|
|||
}
|
||||
else if (node instanceof DomainFileNode fileNode) {
|
||||
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.
|
||||
// Folder-Link status needs to be checked to ensure it corresponds to a folder
|
||||
// internal to the same project.
|
||||
|
@ -154,25 +181,6 @@ public class DataTree extends GTree {
|
|||
return null;
|
||||
}
|
||||
// Get linked folder - status check ensures null will not be returned
|
||||
folder = 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;
|
||||
return linkInfo.getLinkedFolder();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -174,7 +174,9 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
|
|||
private List<GTreeNode> getDomainParentNodes(List<GTreeNode> nodeList) {
|
||||
List<GTreeNode> parentList = new ArrayList<>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,11 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
|
|||
*/
|
||||
public abstract ProjectData getProjectData();
|
||||
|
||||
/**
|
||||
* @returns domain folder/file pathname within project
|
||||
*/
|
||||
public abstract String getPathname();
|
||||
|
||||
@Override
|
||||
public abstract int compareTo(GTreeNode node);
|
||||
|
||||
|
|
|
@ -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 String RIGHT_ARROW = "\u2192";
|
||||
|
||||
// NOTE: We must ensure anything used by sort comparator remains fixed
|
||||
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 Icon icon = UNKNOWN_FILE_ICON;
|
||||
|
@ -54,15 +59,14 @@ public class DomainFileNode extends DataTreeNode {
|
|||
private volatile String toolTipText;
|
||||
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 static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
|
||||
|
||||
DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
|
||||
this.domainFile = domainFile;
|
||||
this.linkInfo = domainFile.getLinkInfo();
|
||||
linkInfo = domainFile.getLinkInfo();
|
||||
isFolderLink = linkInfo != null && linkInfo.isFolderLink();
|
||||
this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
|
||||
displayName = domainFile.getName();
|
||||
refresh();
|
||||
|
@ -84,6 +88,11 @@ public class DomainFileNode extends DataTreeNode {
|
|||
return domainFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathname() {
|
||||
return domainFile.getPathname();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return isLeaf;
|
||||
|
@ -103,15 +112,11 @@ public class DomainFileNode extends DataTreeNode {
|
|||
* @return true if file is a folder-link
|
||||
*/
|
||||
public boolean isFolderLink() {
|
||||
if (linkInfo != null) {
|
||||
return linkInfo.isFolderLink();
|
||||
}
|
||||
return false;
|
||||
return isFolderLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get linked folder which corresponds to this folder-link
|
||||
* (see {@link #isFolderLink()}).
|
||||
* Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}).
|
||||
* @return linked folder or null if this is not a folder-link
|
||||
*/
|
||||
LinkedDomainFolder getLinkedFolder() {
|
||||
|
@ -214,12 +219,20 @@ public class DomainFileNode extends DataTreeNode {
|
|||
private void doRefresh() {
|
||||
|
||||
isLeaf = true;
|
||||
linkInfo = null;
|
||||
LinkFileInfo updatedLinkInfo = domainFile.getLinkInfo();
|
||||
|
||||
boolean brokenLink = false;
|
||||
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<>();
|
||||
LinkStatus linkStatus =
|
||||
LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
|
||||
|
@ -227,7 +240,7 @@ public class DomainFileNode extends DataTreeNode {
|
|||
if (brokenLink) {
|
||||
linkErrors = errors;
|
||||
}
|
||||
else if (isFolderLink()) {
|
||||
else if (isFolderLink) {
|
||||
if (linkStatus == LinkStatus.INTERNAL) {
|
||||
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();
|
||||
|
||||
toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors));
|
||||
|
@ -289,7 +303,25 @@ public class DomainFileNode extends DataTreeNode {
|
|||
|
||||
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)) {
|
||||
try {
|
||||
URL url = new URL(linkPath);
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.util.List;
|
|||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import docking.widgets.tree.GTree;
|
||||
import docking.widgets.tree.GTreeNode;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.util.*;
|
||||
|
@ -48,6 +49,7 @@ public class DomainFolderNode extends DataTreeNode {
|
|||
private boolean isEditable;
|
||||
|
||||
DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) {
|
||||
|
||||
this.domainFolder = domainFolder;
|
||||
this.filter = filter;
|
||||
|
||||
|
@ -73,6 +75,11 @@ public class DomainFolderNode extends DataTreeNode {
|
|||
return domainFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathname() {
|
||||
return domainFolder.getPathname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this node has no children.
|
||||
*/
|
||||
|
|
|
@ -112,17 +112,25 @@ public final class LocalTreeNodeHandler
|
|||
}
|
||||
|
||||
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
|
||||
if (draggedNode instanceof DomainFolderNode folderNode) {
|
||||
// This also checks cases where src/dest projects are using the same repository.
|
||||
// Unfortunately, it will also prevent cases where shared-project folder
|
||||
// does not contain versioned content and could actually be allowed.
|
||||
DomainFolder folder = folderNode.getDomainFolder();
|
||||
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
||||
folder = linkedFolder.getRealFolder();
|
||||
}
|
||||
return !folder.isSameOrAncestor(destFolder);
|
||||
}
|
||||
if (draggedNode instanceof DomainFileNode fileNode) {
|
||||
DomainFolder folder = fileNode.getDomainFile().getParent();
|
||||
DomainFile file = fileNode.getDomainFile();
|
||||
if (file instanceof LinkedDomainFile linkedFile) {
|
||||
file = linkedFile.getRealFile();
|
||||
}
|
||||
DomainFolder folder = file.getParent();
|
||||
if (file.isVersioned()) {
|
||||
// This also checks cases where src/dest projects are using the same repository.
|
||||
return !folder.isSame(destFolder);
|
||||
|
@ -130,6 +138,10 @@ public final class LocalTreeNodeHandler
|
|||
DomainFile destFile = destFolder.getFile(file.getName());
|
||||
return destFile == null || !destFile.equals(file);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -165,6 +177,9 @@ public final class LocalTreeNodeHandler
|
|||
}
|
||||
|
||||
try {
|
||||
if (file instanceof LinkedDomainFile linkedFile) {
|
||||
file = linkedFile.getRealFile();
|
||||
}
|
||||
file.moveTo(destFolder);
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
@ -186,6 +201,9 @@ public final class LocalTreeNodeHandler
|
|||
}
|
||||
|
||||
try {
|
||||
if (sourceFolder instanceof LinkedDomainFolder linkedFolder) {
|
||||
sourceFolder = linkedFolder.getRealFolder();
|
||||
}
|
||||
sourceFolder.moveTo(destFolder);
|
||||
}
|
||||
catch (DuplicateFileException dfe) {
|
||||
|
|
|
@ -17,8 +17,7 @@ package ghidra.framework.main.projectdata.actions;
|
|||
|
||||
import java.awt.Component;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.OptionDialogBuilder;
|
||||
|
@ -65,10 +64,21 @@ public class DeleteProjectFilesTask extends Task {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -77,60 +87,105 @@ public class DeleteProjectFilesTask extends Task {
|
|||
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) {
|
||||
monitor.checkCancelled();
|
||||
deleteFile(file);
|
||||
monitor.incrementProgress(1);
|
||||
}
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// just return so that statistics for what completed can be displayed
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFolders(Set<DomainFolder> folders, TaskMonitor monitor) {
|
||||
|
||||
// If file is contained within a linked-folder (LinkedDomainFile) we need to
|
||||
// use the actual linked file. Since we should be dealing with internally
|
||||
// linked content IOExceptions are unexpected.
|
||||
if (file instanceof LinkedDomainFile linkedFile) {
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// just return so that statistics for what completed can be displayed
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
monitor.checkCancelled();
|
||||
if (!selectedFolders.contains(subFolder)) {
|
||||
deleteFolder(subFolder, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
for (DomainFile file : folder.getFiles()) {
|
||||
monitor.checkCancelled();
|
||||
if (!selectedFiles.contains(file)) {
|
||||
deleteFile(file);
|
||||
deleteFile(file, monitor);
|
||||
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()) {
|
||||
statistics.incrementFileInUse();
|
||||
showFileInUseDialog(file);
|
||||
|
@ -184,7 +245,6 @@ public class DeleteProjectFilesTask extends Task {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
file.delete();
|
||||
statistics.incrementDeleted();
|
||||
}
|
||||
|
@ -198,6 +258,9 @@ public class DeleteProjectFilesTask extends Task {
|
|||
throw new CancelledException();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
monitor.increment();
|
||||
}
|
||||
}
|
||||
|
||||
private int showConfirmDeleteVersionedDialog(DomainFile file) {
|
||||
|
|
|
@ -31,6 +31,8 @@ class FileCountStatistics {
|
|||
private int deleted;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -72,9 +74,10 @@ class FileCountStatistics {
|
|||
|
||||
public void showReport(Component parent) {
|
||||
// don't show results if only one file processed.
|
||||
if (getTotalProcessed() == 1) {
|
||||
if (fileCount == 1 && getTotalProcessed() == 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't show results if all selected files deleted
|
||||
if (deleted == fileCount) {
|
||||
return;
|
||||
|
@ -97,20 +100,24 @@ class FileCountStatistics {
|
|||
builder.append("<tr><td>In Use: </td><td>").append(fileInUse).append("</td></tr>");
|
||||
}
|
||||
if (versionedDeclined > 0) {
|
||||
builder.append("<tr><td> Versioned: </td><td>").append(versionedDeclined).append(
|
||||
"</td></tr>");
|
||||
builder.append("<tr><td> Versioned: </td><td>")
|
||||
.append(versionedDeclined)
|
||||
.append("</td></tr>");
|
||||
}
|
||||
if (checkedOutVersioned > 0) {
|
||||
builder.append("<tr><td>Checked-out: </td><td>").append(checkedOutVersioned).append(
|
||||
"</td></tr>");
|
||||
builder.append("<tr><td>Checked-out: </td><td>")
|
||||
.append(checkedOutVersioned)
|
||||
.append("</td></tr>");
|
||||
}
|
||||
if (readOnlySkipped > 0) {
|
||||
builder.append("<tr><td>Read only: </td><td>").append(readOnlySkipped).append(
|
||||
"</td></tr>");
|
||||
builder.append("<tr><td>Read only: </td><td>")
|
||||
.append(readOnlySkipped)
|
||||
.append("</td></tr>");
|
||||
}
|
||||
if (generalFailure > 0) {
|
||||
builder.append("<tr><td>Other: </td><td>").append(generalFailure).append(
|
||||
"</td></tr>");
|
||||
builder.append("<tr><td>Other: </td><td>")
|
||||
.append(generalFailure)
|
||||
.append("</td></tr>");
|
||||
}
|
||||
builder.append("</table>");
|
||||
}
|
||||
|
|
|
@ -58,13 +58,17 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
|||
|
||||
// Confirm the delete *without* using a task so that do not have 2 dialogs showing
|
||||
int fileCount = countTask.getFileCount();
|
||||
if (!confirmDelete(fileCount, files, context.getComponent())) {
|
||||
if (!confirmDelete(fileCount, files, folders, context.getComponent())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Task 2 - perform the delete--this could take a while
|
||||
DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount);
|
||||
TaskLauncher.launch(deleteTask);
|
||||
|
||||
if (!deleteTask.isCancelled()) {
|
||||
deleteTask.showReport();
|
||||
}
|
||||
}
|
||||
|
||||
DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set<DomainFile> files,
|
||||
|
@ -72,9 +76,10 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
|||
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);
|
||||
int choice = builder.addOption("OK")
|
||||
.addCancel()
|
||||
|
@ -83,28 +88,25 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
|||
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) {
|
||||
return "Are you sure you want to delete the selected empty folder(s)?";
|
||||
}
|
||||
|
||||
if (folders.isEmpty()) {
|
||||
if (fileCount == 1) {
|
||||
if (!selectedFiles.isEmpty()) {
|
||||
DomainFile file = CollectionUtils.any(selectedFiles);
|
||||
String type = file.isLink() ? "link" : "file";
|
||||
DomainFile file = CollectionUtils.any(files);
|
||||
String type = file.getContentType();
|
||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete " + type +
|
||||
" \"" + 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 " +
|
||||
" selected files and folders?";
|
||||
" selected files?";
|
||||
}
|
||||
|
||||
// multiple files selected
|
||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " + fileCount +
|
||||
" selected files?";
|
||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the selected folder(s) and file(s)?";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,8 +25,7 @@ import docking.widgets.tree.GTreeNode;
|
|||
import generic.theme.GIcon;
|
||||
import ghidra.framework.main.datatable.ProjectTreeContext;
|
||||
import ghidra.framework.main.datatree.*;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainFolder;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.util.InvalidNameException;
|
||||
import ghidra.util.exception.AssertException;
|
||||
|
||||
|
@ -48,26 +47,32 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
|
|||
|
||||
@Override
|
||||
protected boolean isEnabledForContext(T context) {
|
||||
try {
|
||||
return getFolder(context).isInWritableProject();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewFolder(T context) {
|
||||
|
||||
DomainFolder parentFolder = getFolder(context);
|
||||
DomainFolder newFolder = createNewFolderWithDefaultName(parentFolder);
|
||||
DomainFolder newFolder = createNewFolderWithDefaultName(context);
|
||||
GTreeNode parent = getParentNode(context);
|
||||
DataTree tree = context.getTree();
|
||||
tree.setEditable(true);
|
||||
tree.startEditing(parent, newFolder.getName());
|
||||
}
|
||||
|
||||
private DomainFolder createNewFolderWithDefaultName(DomainFolder parentFolder) {
|
||||
String name = getNewFolderName(parentFolder);
|
||||
private DomainFolder createNewFolderWithDefaultName(T context) {
|
||||
String errName = "";
|
||||
try {
|
||||
DomainFolder parentFolder = getFolder(context);
|
||||
String name = getNewFolderName(parentFolder);
|
||||
errName = ": " + name;
|
||||
return parentFolder.createFolder(name);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private DomainFolder getFolder(T context) {
|
||||
// the following code relies on the isAddToPopup to ensure that there is exactly one
|
||||
private DomainFolder getFolder(T context) throws IOException {
|
||||
// the following code relied upon by the isAddToPopup to ensure that there is exactly one
|
||||
// file or folder selected
|
||||
DomainFolder folder = null;
|
||||
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);
|
||||
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();
|
||||
return rootNode.getDomainFolder();
|
||||
folder = rootNode.getDomainFolder();
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
private GTreeNode getParentNode(T context) {
|
||||
|
@ -104,9 +124,11 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
|
|||
return context.getTree().getModelRoot();
|
||||
}
|
||||
|
||||
if (node instanceof DomainFileNode) {
|
||||
if (node instanceof DomainFileNode fileNode) {
|
||||
if (!fileNode.isFolderLink()) {
|
||||
return node.getParent();
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,11 @@ public interface DomainFolderChangeListener {
|
|||
|
||||
/**
|
||||
* 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 oldName old name of folder
|
||||
*/
|
||||
|
@ -77,6 +82,11 @@ public interface DomainFolderChangeListener {
|
|||
|
||||
/**
|
||||
* 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 oldParent original parent folder
|
||||
*/
|
||||
|
|
|
@ -59,8 +59,11 @@ public interface LinkFileInfo {
|
|||
* 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
|
||||
* a viewed-project. The resulting folder instance will return true to the method
|
||||
* {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links
|
||||
* which may be chained together.
|
||||
* {@link DomainFolder#isLinked()}.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
@ -71,7 +74,10 @@ public interface LinkFileInfo {
|
|||
* link-file's project or a Ghidra URL.
|
||||
* <P>
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -35,6 +35,6 @@ public interface LinkedDomainFile extends DomainFile {
|
|||
* @return domain file
|
||||
* @throws IOException if IO error occurs or file not found
|
||||
*/
|
||||
public DomainFile getLinkedFile() throws IOException;
|
||||
public DomainFile getRealFile() throws IOException;
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -57,6 +57,8 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
|
|||
|
||||
@Before
|
||||
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");
|
||||
a = root.createFolder("a");
|
||||
b = root.createFolder("b");
|
||||
|
@ -488,9 +490,9 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
|
|||
|
||||
private void runAction() {
|
||||
|
||||
ActionContext context = new ProjectDataContext(/*provider*/null, /*project data*/null,
|
||||
/*context object*/ null, CollectionUtils.asList(folders), CollectionUtils.asList(files),
|
||||
null, true);
|
||||
ActionContext context =
|
||||
new ProjectDataContext(/*provider*/null, /*project data*/null, /*context object*/ null,
|
||||
CollectionUtils.asList(folders), CollectionUtils.asList(files), null, true);
|
||||
performAction(deleteAction, context, false);
|
||||
waitForSwing();
|
||||
}
|
||||
|
|
|
@ -526,7 +526,6 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
|
|||
// of folder or another folder-link-file at the referenced location
|
||||
//
|
||||
String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
|
||||
urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/'
|
||||
|
||||
assertEquals(urlPath, linkInfo.getLinkPath());
|
||||
|
||||
|
@ -593,7 +592,7 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
|
|||
viewTreeHelper.getDomainFileActionContext(f1LinkFile);
|
||||
|
||||
URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
|
||||
"Test", "/f1Link", null);
|
||||
"Test", "/f1Link/", null);
|
||||
|
||||
DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
|
||||
assertNotNull("Copy action not found", copyAction);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import ghidra.program.database.ProgramLinkContentHandler;
|
|||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.server.remote.ServerTestUtil;
|
||||
import ghidra.test.*;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.exception.DuplicateFileException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
|
@ -50,13 +51,16 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
|||
|
||||
/**
|
||||
/abc/ (folder)
|
||||
abc -> /xyz/abc (circular)
|
||||
abc -> /xyz/abc (circular folder allowed as internal)
|
||||
foo (program file)
|
||||
/xyz/
|
||||
abc -> /abc (folder link)
|
||||
abc -> (circular)
|
||||
abc -> /xyz/abc (circular folder allowed as internal)
|
||||
foo
|
||||
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();
|
||||
|
@ -72,6 +76,18 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -257,23 +273,154 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
|||
}
|
||||
|
||||
@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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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(" ", " ");
|
||||
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");
|
||||
assertTrue(abcAbcLinkNode.isFolderLink());
|
||||
String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
|
||||
assertTrue("Unexpected node display name: " + displayName,
|
||||
displayName.endsWith(" /xyz/abc"));
|
||||
assertEquals(LinkStatus.BROKEN,
|
||||
assertEquals(LinkStatus.INTERNAL,
|
||||
LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
|
||||
String tooltip = abcAbcLinkNode.getToolTip().replace(" ", " ");
|
||||
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");
|
||||
assertTrue(xyzAbcLinkNode.isFolderLink());
|
||||
|
@ -283,17 +430,15 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
|||
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");
|
||||
assertTrue(abcLinkedNode.isFolderLink());
|
||||
displayName = runSwing(() -> abcLinkedNode.getDisplayText());
|
||||
assertTrue("Unexpected node display name: " + displayName,
|
||||
displayName.endsWith(" /xyz/abc"));
|
||||
assertEquals(LinkStatus.BROKEN,
|
||||
assertEquals(LinkStatus.INTERNAL,
|
||||
LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
|
||||
tooltip = abcLinkedNode.getToolTip().replace(" ", " ");
|
||||
assertTrue(tooltip.contains("circular"));
|
||||
|
||||
//
|
||||
// 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"));
|
||||
assertEquals(LinkStatus.BROKEN,
|
||||
LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
|
||||
tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
|
||||
String tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
|
||||
assertTrue(tooltip.contains("folder not found: /abc"));
|
||||
|
||||
env.waitForTree(); // give time for ChangeManager to update
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue