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

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

View file

@ -115,7 +115,9 @@
"help/topics/VersionControl/project_repository.htm#SampleCheckOutIcon">checked out</A>.
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>

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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);

View file

@ -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;
}

View file

@ -165,6 +165,16 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
}
}
@Override
public boolean isFolderLink() {
try {
return getFileData().isFolderLink();
}
catch (IOException e) {
return false;
}
}
@Override
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() {

View file

@ -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(

View file

@ -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;
}

View file

@ -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";

View 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();
}
}

View file

@ -57,7 +57,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* since an error will occur if there is no active project at the time the link is followed.
*
* @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

View file

@ -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

View file

@ -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);
}
}
}
}
}

View file

@ -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());
}

View file

@ -0,0 +1,103 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.main;
import java.io.IOException;
import docking.action.MenuData;
import ghidra.framework.main.datatable.FrontendProjectTreeAction;
import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.main.datatree.DataTree;
import ghidra.framework.model.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
public class ProjectDataSelectRealFileOrFolderAction extends FrontendProjectTreeAction {
private FrontEndPlugin plugin;
public ProjectDataSelectRealFileOrFolderAction(FrontEndPlugin plugin, String group) {
super("Select Real File or Folder", plugin.getName());
this.plugin = plugin;
setPopupMenuData(new MenuData(new String[] { "Select Real File" }, group));
setHelpLocation(new HelpLocation("FrontEndPlugin", "Select_Real_File_or_Folder"));
}
@Override
protected void actionPerformed(ProjectDataContext context) {
boolean isFolder = false;
String pathname;
try {
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
DomainFolder folder = context.getSelectedFolders().get(0);
if (!(folder instanceof LinkedDomainFolder linkedFolder)) {
return;
}
isFolder = true;
pathname = linkedFolder.getRealFolder().getPathname();
}
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0);
if (!(file instanceof LinkedDomainFile linkedFile)) {
return;
}
isFolder = false;
pathname = linkedFile.getRealFile().getPathname();
}
else {
return;
}
// Path is local to its project data tree
plugin.showInProjectTree(context.getProjectData(), pathname, isFolder);
}
catch (IOException e) {
Msg.showError(this, null, "Linked Content Error",
"Failed to resolve linked " + (isFolder ? "folder" : "file"), e);
return;
}
}
@Override
protected boolean isEnabledForContext(ProjectDataContext context) {
boolean enabled = false;
String contentType = "Content";
if (context.getComponent() instanceof DataTree) {
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
DomainFolder folder = context.getSelectedFolders().get(0);
if (folder instanceof LinkedDomainFolder) {
contentType = "Folder";
enabled = true;
}
}
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0);
if (file instanceof LinkedDomainFile) {
contentType = "File";
enabled = true;
}
}
}
if (enabled) {
setPopupMenuData(new MenuData(new String[] { "Select Real " + contentType },
getPopupMenuData().getMenuGroup()));
}
return enabled;
}
}

View file

@ -27,6 +27,9 @@ import docking.widgets.tree.GTreeNode;
import ghidra.framework.data.LinkHandler;
import ghidra.framework.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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -46,7 +46,12 @@ public class DomainFileNode extends DataTreeNode {
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
private static final 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);

View file

@ -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.
*/

View file

@ -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) {

View file

@ -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) {

View 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>");
}

View file

@ -58,13 +58,17 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
// Confirm the delete *without* using a task so that do not have 2 dialogs showing
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

View file

@ -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;
}
}

View file

@ -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
*/

View file

@ -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
*/

View file

@ -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;
}

View file

@ -0,0 +1,47 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import static org.junit.Assert.*;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class RelativePathTest extends AbstractGenericTest {
@Test
public void testGetRelativePath() {
// File links
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", false));
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b", "/a/b", false));
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", false));
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", false));
// Folder links
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", true));
assertEquals(".", GhidraFolderData.getRelativePath("/a/b", "/a/b", true));
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", true));
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", true));
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b/", "/a/b", true)); // See Note-1
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/", "/a/b", true));
assertEquals("c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b", true));
assertEquals("../c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b/d", true));
}
}

View file

@ -57,6 +57,8 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
@Before
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();
}

View file

@ -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);

View file

@ -0,0 +1,420 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.main.datatree;
import static org.junit.Assert.*;
import java.util.function.BooleanSupplier;
import org.junit.*;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.listing.Program;
import ghidra.test.*;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
public class ProjectDataTreeTest extends AbstractGhidraHeadedIntegrationTest {
private FrontEndTestEnv env;
private DomainFile programAFile;
private Program program;
@Before
public void setUp() throws Exception {
env = new FrontEndTestEnv();
program = ToyProgramBuilder.buildSimpleProgram("foo", this);
DomainFolder rootFolder = env.getRootFolder();
programAFile = rootFolder.getFile("Program_A");
assertNotNull(programAFile);
}
@After
public void tearDown() throws Exception {
if (program != null) {
program.release(this);
}
env.dispose();
}
@Test
public void testLinkFileUpdate() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("B")));
env.waitForTree();
// Add file 'x' while folder A and linked-folder B are both expanded
aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
//
// Verify good state after everything created
//
// /A
// x
// y -> x
// /B -> /A (linked-folder)
// x
// y -> x
//
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Remove 'x' file and verify broken links are reflected
xNode = (DomainFileNode) aFolderNode.getChild("x");
xNode.getDomainFile().delete();
env.waitForTree();
assertNull(aFolderNode.getChild("x"));
yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate1() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
env.waitForTree();
// Add file 'x' before folder A and is expanded and linked-folder B is not
aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
env.waitForTree();
//
// Verify good state after everything created
//
// /A
// x
// y -> x
// /B -> /A (linked-folder)
// x
// y -> x
//
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Remove 'x' file and verify broken links are reflected
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNotNull(xNode);
xNode.getDomainFile().delete();
env.waitForTree();
assertNull(aFolderNode.getChild("x"));
yNode = (DomainFileNode) aFolderNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
xNode = (DomainFileNode) aFolderNode.getChild("x");
assertNull(xNode);
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate2() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
DomainFolder aFolder = rootFolder.createFolder("A");
// file link created before referenced file
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
ProgramLinkContentHandler.INSTANCE);
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
FolderLinkContentHandler.INSTANCE);
env.waitForTree();
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
env.waitForTree();
// Add file 'x' while linked-folder B is expanded and folder A is not
DomainFile xFile = aFolder.createFile("x", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
//// Verify good state after everything created (leave A collapsed)
DomainFileNode xNode = (DomainFileNode) bFolderLinkNode.getChild("x");
assertNotNull(xNode);
DomainFileNode yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
String tip = yNode.getToolTip();
assertFalse(tip.contains("Broken"));
//// Remove 'x' file
xFile.delete();
env.waitForTree();
assertNull(bFolderLinkNode.getChild("x"));
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
assertNotNull(yNode);
waitForRefresh(yNode);
tip = yNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
@Test
public void testLinkFileUpdate3() throws Exception {
GTree tree = env.getTree();
GTreeNode modelRoot = tree.getModelRoot();
DomainFolder rootFolder = env.getRootFolder();
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
FolderLinkContentHandler.INSTANCE);
DomainFolder usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
env.waitForTree();
// Add file 'bash'
DomainFile bashFile = usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
DomainFileNode binFolderLinkNode = (DomainFileNode) modelRoot.getChild("bin");
assertNotNull(binFolderLinkNode.getChild("bash"));
//
// /bin -> /usr/bin (linked folder)
// bash
// /usr
// /bin
// bash
//
// Delete real folders and content
bashFile.delete();
usrBinFolder.delete(); // /usr/bin
rootFolder.getFolder("usr").delete();
env.waitForTree();
assertNull(binFolderLinkNode.getChild("bash"));
waitForRefresh(binFolderLinkNode);
env.waitForTree();
String tip = binFolderLinkNode.getToolTip();
assertTrue(tip.contains("Broken"));
// binLinkFile.delete();
env.waitForTree();
// Re-create content
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
FolderLinkContentHandler.INSTANCE);
usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
env.waitForTree();
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
env.waitForTree();
program = (Program) programAFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
assertNotNull(program);
usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
program.release(this);
program = null;
env.waitForTree();
DomainFileNode xLinkedFileNode = (DomainFileNode) binFolderLinkNode.getChild("bash");
assertNotNull(xLinkedFileNode);
tip = binFolderLinkNode.getToolTip();
assertFalse(tip.contains("Broken"));
// Repeat removal of folder A and its contents
bashFile = usrBinFolder.getFile("bash");
assertNotNull(bashFile);
bashFile.delete();
usrBinFolder.delete();
rootFolder.getFolder("usr").delete();
env.waitForTree();
assertNull(binFolderLinkNode.getChild("bash"));
waitForRefresh(binFolderLinkNode);
env.waitForTree();
tip = binFolderLinkNode.getToolTip();
assertTrue(tip.contains("Broken"));
}
private void waitForRefresh(DomainFileNode fileNode) {
waitFor(new BooleanSupplier() {
@Override
public boolean getAsBoolean() {
return !fileNode.hasPendingRefresh();
}
});
}
}

View file

@ -32,6 +32,7 @@ import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.listing.Program;
import ghidra.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("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Verify broken folder-link status for /g which has circular reference
//
DomainFileNode gLinkNode = waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Verify broken folder-link status for /f which has circular reference
//
DomainFileNode fLinkNode = waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Rename folder /e to /ABC causing folder-links to have broken path
//
Swing.runNow(() -> eLinkNode.setName("ABC"));
env.waitForTree(); // give time for ChangeManager to update
// Verify /e node not found
assertNull(env.getRootNode().getChild("e"));
//
// Verify broken folder-link status for /ABC (final folder /e not found)
//
DomainFileNode abcLinkNode = waitForFileNode("/ABC");
assertTrue(abcLinkNode.isFolderLink());
displayName = runSwing(() -> abcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
tooltip = abcLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Verify broken folder-link status for /g (final folder /e not found)
//
waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Verify broken folder-link status for /f (final folder /e not found)
//
waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /e"));
//
// Create folder /e
//
DomainFolder rootFolder = env.getRootFolder();
rootFolder.createFolder("e");
env.waitForTree(); // give time for ChangeManager to update
//
// Verify good folder-link status for /ABC
//
waitForFileNode("/ABC");
assertTrue(abcLinkNode.isFolderLink());
displayName = runSwing(() -> abcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
//
// Verify good folder-link status for /g
//
waitForFileNode("/g");
assertTrue(gLinkNode.isFolderLink());
displayName = runSwing(() -> gLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
tooltip = gLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
//
// Verify good folder-link status for /f
//
waitForFileNode("/f");
assertTrue(fLinkNode.isFolderLink());
displayName = runSwing(() -> fLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
tooltip = fLinkNode.getToolTip().replace("&nbsp;", " ");
assertFalse(tooltip.contains("folder not found"));
assertFalse(tooltip.contains("circular"));
}
@Test
public void testCircularFolderLink2() throws Exception {
//
// Verify good folder-link internal status for /abc/abc which has allowed circular reference
//
DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
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("&nbsp;", " ");
assertTrue(tooltip.contains("circular"));
//
// Verify good folder-link internal status for /xyz/abc which has circular reference
// Verify good folder-link internal status for /xyz/abc which has allowed circular reference
//
DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
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("&nbsp;", " ");
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("&nbsp;", " ");
String tooltip = ABCAbcLinkNode.getToolTip().replace("&nbsp;", " ");
assertTrue(tooltip.contains("folder not found: /abc"));
env.waitForTree(); // give time for ChangeManager to update