mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 01:39:21 +02:00
Merge remote-tracking branch 'origin/GP-5907-5908_ghidra1_ProjectTreeUpdateAndDelete--SQUASHED'
This commit is contained in:
commit
f6148f063a
38 changed files with 1988 additions and 515 deletions
|
@ -115,7 +115,9 @@
|
||||||
"help/topics/VersionControl/project_repository.htm#SampleCheckOutIcon">checked out</A>.
|
"help/topics/VersionControl/project_repository.htm#SampleCheckOutIcon">checked out</A>.
|
||||||
In addition, unique icons are used to reflect content-type and if it corresponds to
|
In addition, unique icons are used to reflect content-type and if it corresponds to
|
||||||
a link-file referring to another file or folder (see <A href="#Paste_Link">creating links</A>).
|
a link-file referring to another file or folder (see <A href="#Paste_Link">creating links</A>).
|
||||||
Open this view by activating the project window "Tree View" tab.</P>
|
Open this view by activating the project window "Tree View" tab. Within the tree view
|
||||||
|
internally linked-folders may be expanded to reveal the linked content which corresponds
|
||||||
|
to another folder within the project.</P>
|
||||||
|
|
||||||
<P><IMG src="help/shared/tip.png" border="0">Although Ghidra allows a folder and file within
|
<P><IMG src="help/shared/tip.png" border="0">Although Ghidra allows a folder and file within
|
||||||
the same parent folder to have the same name, it is recommended this be avoided if possible.
|
the same parent folder to have the same name, it is recommended this be avoided if possible.
|
||||||
|
@ -313,6 +315,26 @@
|
||||||
project view once opened.</LI>
|
project view once opened.</LI>
|
||||||
</OL>
|
</OL>
|
||||||
|
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
<H4><A name="Select_Real_File_or_Folder"></A>Select Real File or Folder</H4>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<P>Select a folder or file tree node from an expanded linked-folder or sub-folder
|
||||||
|
node. Content is considered linked if one of its parent nodes corresponds to an
|
||||||
|
expanded folder-link. This linked-content corresponds to a real file or folder
|
||||||
|
contained within another folder. The ability to select the real file or folder
|
||||||
|
may be useful when trying to understand the true origin of such content since this
|
||||||
|
path is not displayed.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<OL>
|
||||||
|
<LI>
|
||||||
|
Select a folder or file tree node from an expanded linked-folder or linked-sub-folder
|
||||||
|
node, right mouse click and choose the <I>Select Real File</I> or <I>Select Real Folder</I>
|
||||||
|
option. The real file or folder will be selected within the tree if possible.</LI>
|
||||||
|
</OL>
|
||||||
|
|
||||||
</BLOCKQUOTE>
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
|
||||||
|
@ -656,7 +678,8 @@
|
||||||
See <A href="#GhidraURLFormats">Ghidra URL formats</A> below.
|
See <A href="#GhidraURLFormats">Ghidra URL formats</A> below.
|
||||||
Since a folder link is stored as a file, it may appear with various icon states which
|
Since a folder link is stored as a file, it may appear with various icon states which
|
||||||
correspond to version control. Folder links only support a single version and may not
|
correspond to version control. Folder links only support a single version and may not
|
||||||
be modified.
|
be modified. The tree may permit expanding such nodes to reveal their linked-content
|
||||||
|
as files and sub-folders.
|
||||||
</TD>
|
</TD>
|
||||||
</TR>
|
</TR>
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ public class ProgramLocator {
|
||||||
try {
|
try {
|
||||||
// Attempt to resolve to actual linked-file to allow for
|
// Attempt to resolve to actual linked-file to allow for
|
||||||
// direct URL reference
|
// direct URL reference
|
||||||
domainFile = linkedFile.getLinkedFile();
|
domainFile = linkedFile.getRealFile();
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
Msg.error(this, "Failed to resolve linked-file", e);
|
Msg.error(this, "Failed to resolve linked-file", e);
|
||||||
|
|
|
@ -18,14 +18,17 @@ package ghidra.app.util.task;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import docking.widgets.OptionDialog;
|
import docking.widgets.OptionDialog;
|
||||||
import ghidra.app.plugin.core.progmgr.ProgramLocator;
|
import ghidra.app.plugin.core.progmgr.ProgramLocator;
|
||||||
import ghidra.app.util.dialog.CheckoutDialog;
|
import ghidra.app.util.dialog.CheckoutDialog;
|
||||||
import ghidra.framework.client.ClientUtil;
|
import ghidra.framework.client.ClientUtil;
|
||||||
import ghidra.framework.client.RepositoryAdapter;
|
import ghidra.framework.client.RepositoryAdapter;
|
||||||
|
import ghidra.framework.data.LinkHandler.LinkStatus;
|
||||||
import ghidra.framework.main.AppInfo;
|
import ghidra.framework.main.AppInfo;
|
||||||
import ghidra.framework.model.DomainFile;
|
import ghidra.framework.model.DomainFile;
|
||||||
|
import ghidra.framework.model.LinkFileInfo;
|
||||||
import ghidra.framework.protocol.ghidra.GhidraURLQuery;
|
import ghidra.framework.protocol.ghidra.GhidraURLQuery;
|
||||||
import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
|
import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
|
||||||
import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter;
|
import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter;
|
||||||
|
@ -142,6 +145,18 @@ public class ProgramOpener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Program openNormal(DomainFile domainFile, TaskMonitor monitor) {
|
private Program openNormal(DomainFile domainFile, TaskMonitor monitor) {
|
||||||
|
|
||||||
|
LinkFileInfo linkInfo = domainFile.getLinkInfo();
|
||||||
|
if (linkInfo != null) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
LinkStatus linkStatus = linkInfo.getLinkStatus(m -> buf.append(m));
|
||||||
|
if (linkStatus == LinkStatus.BROKEN) {
|
||||||
|
Msg.showError(this, null, "Error Opening " + domainFile.getName(),
|
||||||
|
"Failed to open Program Link " + domainFile.getPathname() + "\n" + buf);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String filename = domainFile.getName();
|
String filename = domainFile.getName();
|
||||||
performOptionalCheckout(domainFile, monitor);
|
performOptionalCheckout(domainFile, monitor);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -24,7 +24,8 @@ import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import db.buffers.LocalManagedBufferFile;
|
import db.buffers.LocalManagedBufferFile;
|
||||||
import ghidra.framework.store.*;
|
import ghidra.framework.store.*;
|
||||||
import ghidra.framework.store.local.*;
|
import ghidra.framework.store.local.LocalFileSystem;
|
||||||
|
import ghidra.framework.store.local.LocalFolderItem;
|
||||||
import ghidra.server.Repository;
|
import ghidra.server.Repository;
|
||||||
import ghidra.server.RepositoryManager;
|
import ghidra.server.RepositoryManager;
|
||||||
import ghidra.util.InvalidNameException;
|
import ghidra.util.InvalidNameException;
|
||||||
|
@ -282,11 +283,8 @@ public class RepositoryFolder {
|
||||||
throw new DuplicateFileException(itemName + " already exists");
|
throw new DuplicateFileException(itemName + " already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalTextDataItem textDataItem = fileSystem.createTextDataItem(getPathname(), itemName,
|
fileSystem.createTextDataItem(getPathname(), itemName, fileID, contentType, textData,
|
||||||
fileID, contentType, textData, null); // comment conveyed with Version info below
|
comment, user);
|
||||||
|
|
||||||
Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
|
|
||||||
textDataItem.setVersionInfo(singleVersion);
|
|
||||||
|
|
||||||
RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
|
RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
|
||||||
fileMap.put(itemName, rf);
|
fileMap.put(itemName, rf);
|
||||||
|
|
|
@ -211,13 +211,14 @@ public interface FileSystem {
|
||||||
* @param contentType application defined content type
|
* @param contentType application defined content type
|
||||||
* @param textData text data (required)
|
* @param textData text data (required)
|
||||||
* @param comment file comment (may be null, only used if versioning is enabled)
|
* @param comment file comment (may be null, only used if versioning is enabled)
|
||||||
|
* @param user name of user creating item (required for local versioned item)
|
||||||
* @return new data file
|
* @return new data file
|
||||||
* @throws DuplicateFileException Thrown if a folderItem with that name already exists.
|
* @throws DuplicateFileException Thrown if a folderItem with that name already exists.
|
||||||
* @throws InvalidNameException if the name has illegal characters.
|
* @throws InvalidNameException if the name has illegal characters.
|
||||||
* @throws IOException if an IO error occurs.
|
* @throws IOException if an IO error occurs.
|
||||||
*/
|
*/
|
||||||
public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
|
public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
|
||||||
String contentType, String textData, String comment)
|
String contentType, String textData, String comment, String user)
|
||||||
throws InvalidNameException, IOException;
|
throws InvalidNameException, IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -344,23 +345,22 @@ public interface FileSystem {
|
||||||
*/
|
*/
|
||||||
public static String normalizePath(String path) throws IllegalArgumentException {
|
public static String normalizePath(String path) throws IllegalArgumentException {
|
||||||
if (!path.startsWith(SEPARATOR)) {
|
if (!path.startsWith(SEPARATOR)) {
|
||||||
throw new IllegalArgumentException("Absolute path required");
|
throw new IllegalArgumentException("Absolute path required: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] split = path.split(SEPARATOR);
|
String[] split = path.split(SEPARATOR);
|
||||||
|
|
||||||
ArrayList<String> elements = new ArrayList<>();
|
ArrayList<String> elements = new ArrayList<>();
|
||||||
|
elements.add(SEPARATOR);
|
||||||
for (int i = 1; i < split.length; i++) {
|
for (int i = 1; i < split.length; i++) {
|
||||||
String e = split[i];
|
String e = split[i];
|
||||||
if (e.length() == 0) {
|
if (e.length() == 0) {
|
||||||
throw new IllegalArgumentException("Invalid path with empty element: " + path);
|
throw new IllegalArgumentException("Invalid path with empty element: " + path);
|
||||||
}
|
}
|
||||||
if ("..".equals(e)) {
|
if ("..".equals(e)) {
|
||||||
try {
|
|
||||||
// remove last element
|
// remove last element
|
||||||
elements.removeLast();
|
elements.removeLast();
|
||||||
}
|
if (elements.size() == 0) {
|
||||||
catch (NoSuchElementException ex) {
|
|
||||||
throw new IllegalArgumentException("Invalid path: " + path);
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -369,6 +369,9 @@ public interface FileSystem {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (i < (split.length - 1)) {
|
||||||
|
e += SEPARATOR;
|
||||||
|
}
|
||||||
elements.add(e);
|
elements.add(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -379,9 +382,11 @@ public interface FileSystem {
|
||||||
|
|
||||||
StringBuilder buf = new StringBuilder();
|
StringBuilder buf = new StringBuilder();
|
||||||
for (String e : elements) {
|
for (String e : elements) {
|
||||||
buf.append(SEPARATOR);
|
|
||||||
buf.append(e);
|
buf.append(e);
|
||||||
}
|
}
|
||||||
|
if (path.endsWith(SEPARATOR)) {
|
||||||
|
buf.append(SEPARATOR);
|
||||||
|
}
|
||||||
return buf.toString();
|
return buf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -556,7 +556,7 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name,
|
public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name,
|
||||||
String fileID, String contentType, String textData, String ignoredComment)
|
String fileID, String contentType, String textData, String comment, String user)
|
||||||
throws InvalidNameException, IOException {
|
throws InvalidNameException, IOException {
|
||||||
|
|
||||||
// comment is ignored
|
// comment is ignored
|
||||||
|
@ -573,6 +573,12 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||||
try {
|
try {
|
||||||
ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
|
ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
|
||||||
linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData);
|
linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData);
|
||||||
|
|
||||||
|
if (isVersioned) {
|
||||||
|
Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
|
||||||
|
linkFile.setVersionInfo(singleVersion);
|
||||||
|
}
|
||||||
|
|
||||||
linkFile.log("file created", getUserName());
|
linkFile.log("file created", getUserName());
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
@ -822,30 +828,40 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the full path for a specific folder or item
|
* Returns the full path for a named folder or item within a parent folder
|
||||||
* @param parentPath full parent path
|
* @param parentPath full parent path
|
||||||
* @param name child folder or item name
|
* @param name child folder or item name
|
||||||
* @return pathname
|
* @return pathname
|
||||||
*/
|
*/
|
||||||
protected final static String getPath(String parentPath, String name) {
|
public final static String getPath(String parentPath, String name) {
|
||||||
if (parentPath.length() == 1) {
|
if (parentPath.length() == 1) {
|
||||||
return parentPath + name;
|
return parentPath + name;
|
||||||
}
|
}
|
||||||
return parentPath + SEPARATOR_CHAR + name;
|
return parentPath + SEPARATOR_CHAR + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final static String getParentPath(String path) {
|
/**
|
||||||
|
* Returns the full parent path for a specific folder or item path
|
||||||
|
* @param path full path of folder or item
|
||||||
|
* @return parent path or null if "/" path was specified
|
||||||
|
*/
|
||||||
|
public final static String getParentPath(String path) {
|
||||||
|
int index = path.lastIndexOf(SEPARATOR_CHAR);
|
||||||
|
if (index == 0) {
|
||||||
if (path.length() == 1) {
|
if (path.length() == 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int index = path.lastIndexOf(SEPARATOR_CHAR);
|
|
||||||
if (index == 0) {
|
|
||||||
return SEPARATOR;
|
return SEPARATOR;
|
||||||
}
|
}
|
||||||
return path.substring(0, index);
|
return path.substring(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final static String getName(String path) {
|
/**
|
||||||
|
* Returns the name for a specific folder or item path
|
||||||
|
* @param path full path of folder or item
|
||||||
|
* @return parent path or null if "/" path was specified
|
||||||
|
*/
|
||||||
|
public final static String getName(String path) {
|
||||||
if (path.length() == 1) {
|
if (path.length() == 1) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,14 +89,14 @@ public abstract class LocalFolderItem implements FolderItem {
|
||||||
* @param useDataDir if true the getDataDir() method must return an appropriate
|
* @param useDataDir if true the getDataDir() method must return an appropriate
|
||||||
* directory for data storage.
|
* directory for data storage.
|
||||||
* @param create if true the data directory will be created
|
* @param create if true the data directory will be created
|
||||||
* @throws IOException
|
* @throws IOException if an IO error occurs
|
||||||
*/
|
*/
|
||||||
LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir,
|
LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir,
|
||||||
boolean create) throws IOException {
|
boolean create) throws IOException {
|
||||||
this.fileSystem = fileSystem;
|
this.fileSystem = fileSystem;
|
||||||
this.propertyFile = propertyFile;
|
this.propertyFile = propertyFile;
|
||||||
this.isVersioned = fileSystem.isVersioned();
|
this.isVersioned = fileSystem.isVersioned();
|
||||||
this.useDataDir = useDataDir || isVersioned;
|
this.useDataDir = useDataDir;
|
||||||
|
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -229,7 +229,7 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
|
public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
|
||||||
String contentType, String textData, String comment)
|
String contentType, String textData, String comment, String ignoredUser)
|
||||||
throws InvalidNameException, IOException {
|
throws InvalidNameException, IOException {
|
||||||
repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment);
|
repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment);
|
||||||
return (TextDataItem) getItem(parentPath, name);
|
return (TextDataItem) getItem(parentPath, name);
|
||||||
|
|
|
@ -729,6 +729,10 @@ public class DefaultProjectData implements ProjectData {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refresh(boolean force) {
|
public void refresh(boolean force) {
|
||||||
|
// FIXME: We ignore force. We are forcing full recursive refresh on non-visited folders
|
||||||
|
// only - seems inconsistent!!
|
||||||
|
// Underlying method fails if recursive and force is false.
|
||||||
|
// NOTE: Refresh really does nothing if force is false and folder already visited
|
||||||
try {
|
try {
|
||||||
rootFolderData.refresh(true, true, projectDisposalMonitor);
|
rootFolderData.refresh(true, true, projectDisposalMonitor);
|
||||||
}
|
}
|
||||||
|
@ -1057,7 +1061,8 @@ public class DefaultProjectData implements ProjectData {
|
||||||
@Override
|
@Override
|
||||||
public void folderCreated(final String parentPath, final String name) {
|
public void folderCreated(final String parentPath, final String name) {
|
||||||
synchronized (fileSystem) {
|
synchronized (fileSystem) {
|
||||||
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, true);
|
boolean lazy = !rootFolderData.mustVisit(parentPath);
|
||||||
|
GhidraFolderData folderData = rootFolderData.getFolderPathData(parentPath, lazy);
|
||||||
if (folderData != null) {
|
if (folderData != null) {
|
||||||
try {
|
try {
|
||||||
folderData.folderChanged(name);
|
folderData.folderChanged(name);
|
||||||
|
@ -1111,7 +1116,9 @@ public class DefaultProjectData implements ProjectData {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
folderData = rootFolderData.getFolderPathData(newParentPath, true);
|
|
||||||
|
boolean lazy = !rootFolderData.mustVisit(newParentPath);
|
||||||
|
folderData = rootFolderData.getFolderPathData(newParentPath, lazy);
|
||||||
if (folderData != null) {
|
if (folderData != null) {
|
||||||
try {
|
try {
|
||||||
folderData.folderChanged(folderName);
|
folderData.folderChanged(folderName);
|
||||||
|
@ -1338,7 +1345,7 @@ public class DefaultProjectData implements ProjectData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GhidraFolderData getRootFolderData() {
|
RootGhidraFolderData getRootFolderData() {
|
||||||
return rootFolderData;
|
return rootFolderData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -165,6 +165,16 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFolderLink() {
|
||||||
|
try {
|
||||||
|
return getFileData().isFolderLink();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LinkFileInfo getLinkInfo() {
|
public LinkFileInfo getLinkInfo() {
|
||||||
return isLink() ? this : null;
|
return isLink() ? this : null;
|
||||||
|
@ -642,7 +652,7 @@ public class GhidraFile implements DomainFile, LinkFileInfo {
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
fileError(e);
|
fileError(e);
|
||||||
}
|
}
|
||||||
return new HashMap<>();
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
void fileChanged() {
|
void fileChanged() {
|
||||||
|
|
|
@ -81,7 +81,12 @@ public class GhidraFileData {
|
||||||
private GhidraFolderData parent;
|
private GhidraFolderData parent;
|
||||||
private String name;
|
private String name;
|
||||||
private String fileID;
|
private String fileID;
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
private String linkPath;
|
private String linkPath;
|
||||||
|
private String absoluteLinkPath;
|
||||||
|
private boolean isLink;
|
||||||
|
private boolean isFolderLink;
|
||||||
|
|
||||||
private LocalFolderItem folderItem;
|
private LocalFolderItem folderItem;
|
||||||
private FolderItem versionedFolderItem;
|
private FolderItem versionedFolderItem;
|
||||||
|
@ -98,7 +103,10 @@ public class GhidraFileData {
|
||||||
// longer used.
|
// longer used.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a file instance with a specified name and a correpsonding parent folder
|
* Construct a file instance with a specified name and a correpsonding parent folder.
|
||||||
|
* It is important that this object only be instantiated by the
|
||||||
|
* {@link GhidraFolderData} parent supplied and properly cached and tracked to ensure proper
|
||||||
|
* tracking and link registration.
|
||||||
* @param parent parent folder
|
* @param parent parent folder
|
||||||
* @param name file name
|
* @param name file name
|
||||||
* @throws IOException if an IO error occurs
|
* @throws IOException if an IO error occurs
|
||||||
|
@ -118,7 +126,9 @@ public class GhidraFileData {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new file instance with a specified name and a corresponding parent folder using
|
* Construct a new file instance with a specified name and a corresponding parent folder using
|
||||||
* up-to-date folder items.
|
* up-to-date folder items. It is important that this object only be instantiated by the
|
||||||
|
* {@link GhidraFolderData} parent supplied and properly cached and tracked to ensure proper
|
||||||
|
* tracking and link registration.
|
||||||
* @param parent parent folder
|
* @param parent parent folder
|
||||||
* @param name file name
|
* @param name file name
|
||||||
* @param folderItem local folder item
|
* @param folderItem local folder item
|
||||||
|
@ -138,10 +148,15 @@ public class GhidraFileData {
|
||||||
|
|
||||||
validateCheckout();
|
validateCheckout();
|
||||||
updateFileID();
|
updateFileID();
|
||||||
|
|
||||||
|
registerLinkFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) {
|
void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) {
|
||||||
linkPath = null;
|
|
||||||
|
unregisterLinkFile();
|
||||||
|
|
||||||
|
contentType = null;
|
||||||
icon = null;
|
icon = null;
|
||||||
disabledIcon = null;
|
disabledIcon = null;
|
||||||
|
|
||||||
|
@ -151,6 +166,8 @@ public class GhidraFileData {
|
||||||
validateCheckout();
|
validateCheckout();
|
||||||
boolean fileIDset = updateFileID();
|
boolean fileIDset = updateFileID();
|
||||||
|
|
||||||
|
registerLinkFile();
|
||||||
|
|
||||||
if (parent.visited()) {
|
if (parent.visited()) {
|
||||||
// NOTE: we should maintain some cached data so we can determine if something really changed
|
// NOTE: we should maintain some cached data so we can determine if something really changed
|
||||||
listener.domainFileStatusChanged(getDomainFile(), fileIDset);
|
listener.domainFileStatusChanged(getDomainFile(), fileIDset);
|
||||||
|
@ -158,7 +175,13 @@ public class GhidraFileData {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean refresh() throws IOException {
|
private boolean refresh() throws IOException {
|
||||||
linkPath = null;
|
|
||||||
|
unregisterLinkFile();
|
||||||
|
|
||||||
|
contentType = null;
|
||||||
|
icon = null;
|
||||||
|
disabledIcon = null;
|
||||||
|
|
||||||
String parentPath = parent.getPathname();
|
String parentPath = parent.getPathname();
|
||||||
if (folderItem == null) {
|
if (folderItem == null) {
|
||||||
folderItem = fileSystem.getItem(parentPath, name);
|
folderItem = fileSystem.getItem(parentPath, name);
|
||||||
|
@ -183,7 +206,54 @@ public class GhidraFileData {
|
||||||
if (folderItem == null && versionedFolderItem == null) {
|
if (folderItem == null && versionedFolderItem == null) {
|
||||||
throw new FileNotFoundException(name + " not found");
|
throw new FileNotFoundException(name + " not found");
|
||||||
}
|
}
|
||||||
return updateFileID();
|
|
||||||
|
boolean fileIDset = updateFileID();
|
||||||
|
|
||||||
|
registerLinkFile();
|
||||||
|
|
||||||
|
return fileIDset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerLinkFile() {
|
||||||
|
try {
|
||||||
|
ContentHandler<?> contentHandler = getContentHandler();
|
||||||
|
isLink = LinkHandler.class.isAssignableFrom(contentHandler.getClass());
|
||||||
|
if (!isLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isFolderLink =
|
||||||
|
FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType());
|
||||||
|
|
||||||
|
getLinkPath(true); // will cache linkPath and absoluteLinkPath
|
||||||
|
|
||||||
|
if (absoluteLinkPath == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid registering circular reference
|
||||||
|
if (isFolderLink && getPathname().startsWith(absoluteLinkPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
|
||||||
|
rootFolderData.registerInternalLinkPath(absoluteLinkPath);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// Too much noise if we report IOExceptions. If it happens to one file it could happen
|
||||||
|
// with many files.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterLinkFile() {
|
||||||
|
if (absoluteLinkPath != null) {
|
||||||
|
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
|
||||||
|
rootFolderData.unregisterInternalLinkPath(absoluteLinkPath);
|
||||||
|
}
|
||||||
|
linkPath = null;
|
||||||
|
absoluteLinkPath = null;
|
||||||
|
isLink = false;
|
||||||
|
isFolderLink = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean updateFileID() {
|
private boolean updateFileID() {
|
||||||
|
@ -204,8 +274,6 @@ public class GhidraFileData {
|
||||||
if (mergeInProgress) {
|
if (mergeInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
icon = null;
|
|
||||||
disabledIcon = null;
|
|
||||||
fileIDset |= refresh();
|
fileIDset |= refresh();
|
||||||
if (parent.visited()) {
|
if (parent.visited()) {
|
||||||
// NOTE: we should maintain some cached data so we can determine if something really changed
|
// NOTE: we should maintain some cached data so we can determine if something really changed
|
||||||
|
@ -267,6 +335,7 @@ public class GhidraFileData {
|
||||||
*/
|
*/
|
||||||
void dispose() {
|
void dispose() {
|
||||||
projectData.removeFromIndex(fileID);
|
projectData.removeFromIndex(fileID);
|
||||||
|
unregisterLinkFile();
|
||||||
// NOTE: clearing the following can cause issues since there may be some residual
|
// NOTE: clearing the following can cause issues since there may be some residual
|
||||||
// activity/use which will get a NPE
|
// activity/use which will get a NPE
|
||||||
// parent = null;
|
// parent = null;
|
||||||
|
@ -434,10 +503,6 @@ public class GhidraFileData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isFolderLink() {
|
|
||||||
return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns content-type string for this file
|
* Returns content-type string for this file
|
||||||
* @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
|
* @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
|
||||||
|
@ -445,14 +510,22 @@ public class GhidraFileData {
|
||||||
*/
|
*/
|
||||||
String getContentType() {
|
String getContentType() {
|
||||||
synchronized (fileSystem) {
|
synchronized (fileSystem) {
|
||||||
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
|
if (contentType != null) {
|
||||||
// this can happen when we are trying to load a version file from
|
return contentType;
|
||||||
// a server to which we are not connected
|
|
||||||
if (item == null) {
|
|
||||||
return ContentHandler.MISSING_CONTENT;
|
|
||||||
}
|
}
|
||||||
String contentType = item.getContentType();
|
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
|
||||||
return contentType != null ? contentType : ContentHandler.UNKNOWN_CONTENT;
|
if (item == null) {
|
||||||
|
// This can happen when we are trying to load a version file from
|
||||||
|
// a server to which we are not connected
|
||||||
|
contentType = ContentHandler.MISSING_CONTENT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contentType = item.getContentType();
|
||||||
|
if (contentType == null) {
|
||||||
|
contentType = ContentHandler.UNKNOWN_CONTENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1235,7 +1308,7 @@ public class GhidraFileData {
|
||||||
else if (folderItem instanceof TextDataItem textDataItem) {
|
else if (folderItem instanceof TextDataItem textDataItem) {
|
||||||
versionedFileSystem.createTextDataItem(parentPath, name,
|
versionedFileSystem.createTextDataItem(parentPath, name,
|
||||||
folderItem.getFileID(), folderItem.getContentType(),
|
folderItem.getFileID(), folderItem.getContentType(),
|
||||||
textDataItem.getTextData(), comment);
|
textDataItem.getTextData(), comment, user);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
|
@ -2237,17 +2310,26 @@ public class GhidraFileData {
|
||||||
* @return true if link file else false for a normal domain file
|
* @return true if link file else false for a normal domain file
|
||||||
*/
|
*/
|
||||||
boolean isLink() {
|
boolean isLink() {
|
||||||
try {
|
return isLink; // relies on refresh to initialize
|
||||||
return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this file is a link file which corresponds to a folder link.
|
||||||
|
* If this is a folder-link it should not be used to obtain a {@link DomainObject}.
|
||||||
|
* The link path or URL stored within the link-file may be read using {@link #getLinkPath(boolean)}.
|
||||||
|
* The content type (see {@link #getContentType()} of a folder-link will be
|
||||||
|
* {@link FolderLinkContentHandler}.
|
||||||
|
* @return true if link file else false for a normal domain file
|
||||||
|
*/
|
||||||
|
boolean isFolderLink() {
|
||||||
|
return isFolderLink; // relies on refresh to initialize
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this is a {@link #isLink() link file} this method will return the link-path which
|
* If this is a {@link #isLink() link file} this method will return the link-path which
|
||||||
* may be either an absolute or relative path within the the project or a Ghidra URL.
|
* may be either an absolute or relative path within the the project or a Ghidra URL.
|
||||||
|
* Invoking with {@code resolve==true} will ensure that both {@code linkPath} and
|
||||||
|
* {@code absoluteLinkPath} get properly cached.
|
||||||
*
|
*
|
||||||
* @param resolve if true relative paths will always be converted to an absolute path
|
* @param resolve if true relative paths will always be converted to an absolute path
|
||||||
* @return associated link path or null if not a link file
|
* @return associated link path or null if not a link file
|
||||||
|
@ -2275,12 +2357,19 @@ public class GhidraFileData {
|
||||||
return linkPath;
|
return linkPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = linkPath;
|
|
||||||
if (!GhidraURL.isGhidraURL(linkPath)) {
|
if (!GhidraURL.isGhidraURL(linkPath)) {
|
||||||
path = getAbsolutePath(linkPath);
|
if (absoluteLinkPath == null) {
|
||||||
|
try {
|
||||||
|
absoluteLinkPath = getAbsolutePath(linkPath);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return absoluteLinkPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return linkPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getAbsolutePath(String path) throws IOException {
|
private String getAbsolutePath(String path) throws IOException {
|
||||||
|
@ -2292,13 +2381,7 @@ public class GhidraFileData {
|
||||||
}
|
}
|
||||||
absPath += path;
|
absPath += path;
|
||||||
}
|
}
|
||||||
try {
|
return FileSystem.normalizePath(absPath);
|
||||||
absPath = FileSystem.normalizePath(absPath);
|
|
||||||
}
|
|
||||||
catch (IllegalArgumentException e) {
|
|
||||||
throw new IOException("Invalid link path: " + linkPath);
|
|
||||||
}
|
|
||||||
return absPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2396,7 +2479,7 @@ public class GhidraFileData {
|
||||||
if (!StringUtils.isBlank(lp)) {
|
if (!StringUtils.isBlank(lp)) {
|
||||||
newParent.getLocalFileSystem()
|
newParent.getLocalFileSystem()
|
||||||
.createTextDataItem(pathname, targetName,
|
.createTextDataItem(pathname, targetName,
|
||||||
FileIDFactory.createFileID(), contentType, lp, null);
|
FileIDFactory.createFileID(), contentType, lp, null, null);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
|
|
|
@ -64,7 +64,7 @@ class GhidraFolderData {
|
||||||
// folderList and fileList are only be used if visited is true
|
// folderList and fileList are only be used if visited is true
|
||||||
private Set<String> folderList = new TreeSet<>();
|
private Set<String> folderList = new TreeSet<>();
|
||||||
|
|
||||||
private boolean visited; // true if full refresh was performed
|
private boolean visited; // true if full refresh was performed and change notifications get sent
|
||||||
|
|
||||||
private Map<String, GhidraFileData> fileDataCache = new HashMap<>();
|
private Map<String, GhidraFileData> fileDataCache = new HashMap<>();
|
||||||
private Map<String, GhidraFolderData> folderDataCache = new HashMap<>();
|
private Map<String, GhidraFolderData> folderDataCache = new HashMap<>();
|
||||||
|
@ -122,6 +122,15 @@ class GhidraFolderData {
|
||||||
return visited;
|
return visited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this folder must be visited when created to ensure that related change
|
||||||
|
* notifications are properly conveyed.
|
||||||
|
*/
|
||||||
|
boolean mustVisit() {
|
||||||
|
RootGhidraFolderData rootFolderData = projectData.getRootFolderData();
|
||||||
|
return rootFolderData.mustVisit(getPathname());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return local file system
|
* @return local file system
|
||||||
*/
|
*/
|
||||||
|
@ -283,7 +292,7 @@ class GhidraFolderData {
|
||||||
parent.folderList.remove(oldName);
|
parent.folderList.remove(oldName);
|
||||||
parent.folderList.add(newName);
|
parent.folderList.add(newName);
|
||||||
|
|
||||||
// Must force refresh to ensure that all folder items are properly updted with new parent path
|
// Must force refresh to ensure that all folder items are properly updated with new parent path
|
||||||
refresh(true, true, projectData.getProjectDisposalMonitor());
|
refresh(true, true, projectData.getProjectDisposalMonitor());
|
||||||
|
|
||||||
listener.domainFolderRenamed(newFolder, oldName);
|
listener.domainFolderRenamed(newFolder, oldName);
|
||||||
|
@ -385,9 +394,7 @@ class GhidraFolderData {
|
||||||
* @param newFileName file name after rename
|
* @param newFileName file name after rename
|
||||||
*/
|
*/
|
||||||
void fileRenamed(String oldFileName, String newFileName) {
|
void fileRenamed(String oldFileName, String newFileName) {
|
||||||
GhidraFileData fileData;
|
GhidraFileData fileData = fileDataCache.remove(oldFileName);
|
||||||
synchronized (fileSystem) {
|
|
||||||
fileData = fileDataCache.remove(oldFileName);
|
|
||||||
if (fileData == null || this != fileData.getParent() ||
|
if (fileData == null || this != fileData.getParent() ||
|
||||||
!newFileName.equals(fileData.getName())) {
|
!newFileName.equals(fileData.getName())) {
|
||||||
throw new AssertException();
|
throw new AssertException();
|
||||||
|
@ -397,7 +404,6 @@ class GhidraFolderData {
|
||||||
listener.domainFileRenamed(getDomainFile(newFileName), oldFileName);
|
listener.domainFileRenamed(getDomainFile(newFileName), oldFileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update file list/cache based upon change of parent for a file.
|
* Update file list/cache based upon change of parent for a file.
|
||||||
|
@ -510,8 +516,7 @@ class GhidraFolderData {
|
||||||
* if this folder has been visited
|
* if this folder has been visited
|
||||||
* @param folderName name of folder which was removed
|
* @param folderName name of folder which was removed
|
||||||
*/
|
*/
|
||||||
void folderRemoved(String folderName) {
|
private void folderRemoved(String folderName) {
|
||||||
synchronized (fileSystem) {
|
|
||||||
GhidraFolderData folderData = folderDataCache.remove(folderName);
|
GhidraFolderData folderData = folderDataCache.remove(folderName);
|
||||||
if (folderData != null) {
|
if (folderData != null) {
|
||||||
folderData.dispose();
|
folderData.dispose();
|
||||||
|
@ -520,7 +525,6 @@ class GhidraFolderData {
|
||||||
listener.domainFolderRemoved(getDomainFolder(), folderName);
|
listener.domainFolderRemoved(getDomainFolder(), folderName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the cached data for this folder and all of its children recursively.
|
* Disposes the cached data for this folder and all of its children recursively.
|
||||||
|
@ -561,6 +565,8 @@ class GhidraFolderData {
|
||||||
*/
|
*/
|
||||||
private void refreshFolders(boolean recursive, TaskMonitor monitor) throws IOException {
|
private void refreshFolders(boolean recursive, TaskMonitor monitor) throws IOException {
|
||||||
|
|
||||||
|
// FIXME: inconsistent use of forced-recursive refresh and cached folderList
|
||||||
|
|
||||||
String path = getPathname();
|
String path = getPathname();
|
||||||
HashSet<String> newSet = new HashSet<>();
|
HashSet<String> newSet = new HashSet<>();
|
||||||
|
|
||||||
|
@ -772,6 +778,10 @@ class GhidraFolderData {
|
||||||
void refresh(boolean recursive, boolean force, TaskMonitor monitor) throws IOException {
|
void refresh(boolean recursive, boolean force, TaskMonitor monitor) throws IOException {
|
||||||
synchronized (fileSystem) {
|
synchronized (fileSystem) {
|
||||||
if (recursive && !force) {
|
if (recursive && !force) {
|
||||||
|
|
||||||
|
// FIXME: Why must this restriction be imposed. We need a lazy refresh that only refreshes
|
||||||
|
// those folders that have been visited or must be visited.
|
||||||
|
|
||||||
throw new IllegalArgumentException("force must be true when recursive");
|
throw new IllegalArgumentException("force must be true when recursive");
|
||||||
}
|
}
|
||||||
if (monitor != null && monitor.isCancelled()) {
|
if (monitor != null && monitor.isCancelled()) {
|
||||||
|
@ -780,6 +790,9 @@ class GhidraFolderData {
|
||||||
if (visited && !force) {
|
if (visited && !force) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visited = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateExistenceState();
|
updateExistenceState();
|
||||||
}
|
}
|
||||||
|
@ -797,7 +810,8 @@ class GhidraFolderData {
|
||||||
throw new FileNotFoundException("Folder not found: " + getPathname());
|
throw new FileNotFoundException("Folder not found: " + getPathname());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// FIXME: If forced we should be refreshing folder/file lists
|
||||||
|
|
||||||
refreshFiles(monitor);
|
refreshFiles(monitor);
|
||||||
|
|
||||||
if (monitor != null && monitor.isCancelled()) {
|
if (monitor != null && monitor.isCancelled()) {
|
||||||
|
@ -806,10 +820,6 @@ class GhidraFolderData {
|
||||||
|
|
||||||
refreshFolders(recursive, monitor);
|
refreshFolders(recursive, monitor);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
visited = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -843,8 +853,11 @@ class GhidraFolderData {
|
||||||
try {
|
try {
|
||||||
folderData = new GhidraFolderData(this, folderName);
|
folderData = new GhidraFolderData(this, folderName);
|
||||||
folderDataCache.put(folderName, folderData);
|
folderDataCache.put(folderName, folderData);
|
||||||
|
if (folderData.mustVisit()) {
|
||||||
|
folderData.refresh(false, true, TaskMonitor.DUMMY);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException e) {
|
}
|
||||||
|
catch (IOException e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1226,6 +1239,10 @@ class GhidraFolderData {
|
||||||
GhidraFolder newFolder = getDomainFolder();
|
GhidraFolder newFolder = getDomainFolder();
|
||||||
|
|
||||||
if (parent.visited || newParent.visited) {
|
if (parent.visited || newParent.visited) {
|
||||||
|
|
||||||
|
// Must force refresh to ensure that all folder items are properly updated with new parent path
|
||||||
|
refresh(true, true, projectData.getProjectDisposalMonitor());
|
||||||
|
|
||||||
listener.domainFolderMoved(newFolder, oldParent);
|
listener.domainFolderMoved(newFolder, oldParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1398,17 +1415,34 @@ class GhidraFolderData {
|
||||||
String linkFilename, LinkHandler<?> lh) throws IOException {
|
String linkFilename, LinkHandler<?> lh) throws IOException {
|
||||||
synchronized (fileSystem) {
|
synchronized (fileSystem) {
|
||||||
if (fileSystem.isReadOnly()) {
|
if (fileSystem.isReadOnly()) {
|
||||||
throw new ReadOnlyException("copyAsLink permitted to writeable project only");
|
throw new ReadOnlyException("createLinkFile permitted to writeable project only");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean referenceMyProject = (sourceProjectData == projectData);
|
||||||
|
boolean isFolderLink = (lh instanceof FolderLinkContentHandler);
|
||||||
|
|
||||||
if (!pathname.startsWith(FileSystem.SEPARATOR)) {
|
if (!pathname.startsWith(FileSystem.SEPARATOR)) {
|
||||||
throw new IllegalArgumentException("invalid pathname specified");
|
throw new IllegalArgumentException(
|
||||||
|
"invalid absolute pathname specified: " + pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFolderLink) {
|
||||||
|
// Force folder link path to end with "/" for GhidraURL case to disambiguate
|
||||||
|
if (!referenceMyProject && !pathname.endsWith(FileSystem.SEPARATOR)) {
|
||||||
|
pathname += FileSystem.SEPARATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pathname.endsWith(FileSystem.SEPARATOR) || pathname.endsWith("/.") ||
|
||||||
|
pathname.endsWith("/..")) {
|
||||||
|
throw new IllegalArgumentException("invalid file pathname specified: " + pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
pathname = FileSystem.normalizePath(pathname);
|
||||||
|
|
||||||
String linkPath;
|
String linkPath;
|
||||||
if (sourceProjectData == projectData) {
|
if (referenceMyProject) {
|
||||||
if (makeRelative) {
|
if (makeRelative) {
|
||||||
linkPath = getRelativePath(pathname, getPathname());
|
linkPath = getRelativePath(pathname, getPathname(), isFolderLink);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
linkPath = pathname;
|
linkPath = pathname;
|
||||||
|
@ -1495,12 +1529,48 @@ class GhidraFolderData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getRelativePath(String referencedPathname, String linkParentPathname) {
|
/**
|
||||||
Path referencedPath = Paths.get(referencedPathname);
|
*
|
||||||
|
* @param normalizedReferencedPathname an absolute normalized folder/file reference path
|
||||||
|
* (see {@link FileSystem#normalizePath(String)}).
|
||||||
|
* @param linkParentPathname an absolute Ghidra folder pathname which will be the origin
|
||||||
|
* of the returned relative path and will be the folder where the lin-file is stored.
|
||||||
|
* @param isFolderRef true if {@code normalizedReferencedPathname} refers to a folder, else false
|
||||||
|
* @return relative path
|
||||||
|
* @throws IllegalArgumentException if referenced path cannot be relativized. This should not
|
||||||
|
* occur if absolute normalized path arguments are properly formed and are legal.
|
||||||
|
*/
|
||||||
|
static String getRelativePath(String normalizedReferencedPathname, String linkParentPathname,
|
||||||
|
boolean isFolderRef) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
String finalRefElement = null;
|
||||||
|
if (!isFolderRef && !normalizedReferencedPathname.endsWith(FileSystem.SEPARATOR)) {
|
||||||
|
// Preserve last element name which may not be a folder name if not within root folder
|
||||||
|
int lastSepIx = normalizedReferencedPathname.lastIndexOf(FileSystem.SEPARATOR);
|
||||||
|
if (lastSepIx != 0) {
|
||||||
|
finalRefElement = normalizedReferencedPathname.substring(lastSepIx + 1);
|
||||||
|
normalizedReferencedPathname = normalizedReferencedPathname.substring(0, lastSepIx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path referencedPath = Paths.get(normalizedReferencedPathname);
|
||||||
Path linkParentPath = Paths.get(linkParentPathname);
|
Path linkParentPath = Paths.get(linkParentPathname);
|
||||||
Path relativePath = linkParentPath.relativize(referencedPath);
|
Path relativePath = linkParentPath.relativize(referencedPath);
|
||||||
String path = relativePath.toString();
|
String path = relativePath.toString();
|
||||||
if (referencedPathname.endsWith(FileSystem.SEPARATOR) &&
|
|
||||||
|
// Re-apply preserved finalRefElement to relative path
|
||||||
|
if (finalRefElement != null) {
|
||||||
|
if (!path.isBlank()) {
|
||||||
|
path += FileSystem.SEPARATOR;
|
||||||
|
}
|
||||||
|
path += finalRefElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isBlank()) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedReferencedPathname.endsWith(FileSystem.SEPARATOR) &&
|
||||||
!path.endsWith(FileSystem.SEPARATOR)) {
|
!path.endsWith(FileSystem.SEPARATOR)) {
|
||||||
path += FileSystem.SEPARATOR;
|
path += FileSystem.SEPARATOR;
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
String linkFilename) throws IOException, InvalidNameException {
|
String linkFilename) throws IOException, InvalidNameException {
|
||||||
|
|
||||||
fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(),
|
fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(),
|
||||||
getContentType(), linkPath, null);
|
getContentType(), linkPath, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -334,9 +334,20 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = linkPath;
|
String path = linkPath;
|
||||||
|
boolean isRelative = false;
|
||||||
if (!GhidraURL.isGhidraURL(path)) {
|
if (!GhidraURL.isGhidraURL(path)) {
|
||||||
if (!linkPath.startsWith(FileSystem.SEPARATOR)) {
|
if (!linkPath.startsWith(FileSystem.SEPARATOR)) {
|
||||||
path = linkFile.getParent().getPathname();
|
isRelative = true;
|
||||||
|
DomainFolder parent;
|
||||||
|
if (linkFile instanceof LinkedDomainFile linkedFile) {
|
||||||
|
// Relative to real file's parent
|
||||||
|
parent = linkedFile.getRealFile().getParent();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Relative to link-file's parent
|
||||||
|
parent = linkFile.getParent();
|
||||||
|
}
|
||||||
|
path = parent.getPathname();
|
||||||
if (!path.endsWith(FileSystem.SEPARATOR)) {
|
if (!path.endsWith(FileSystem.SEPARATOR)) {
|
||||||
path += FileSystem.SEPARATOR;
|
path += FileSystem.SEPARATOR;
|
||||||
}
|
}
|
||||||
|
@ -346,7 +357,11 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
return FileSystem.normalizePath(path);
|
return FileSystem.normalizePath(path);
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
throw new IOException("Invalid link path: " + linkPath);
|
String hint = "";
|
||||||
|
if (isRelative && linkFile instanceof LinkedDomainFile) {
|
||||||
|
hint = " (relative to real link-file)";
|
||||||
|
}
|
||||||
|
throw new IOException("Invalid link path: " + path + hint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
|
@ -370,11 +385,16 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
/**
|
/**
|
||||||
* Add real internal folder path for specified folder or folder-link and check for
|
* Add real internal folder path for specified folder or folder-link and check for
|
||||||
* circular conflict.
|
* circular conflict.
|
||||||
|
*
|
||||||
|
* NOTE: This is only useful in detecting a self-referencing
|
||||||
|
* path and not those that involve multiple independent linked-folders that could
|
||||||
|
* form circular paths.
|
||||||
|
*
|
||||||
* @param pathSet real path accumulator
|
* @param pathSet real path accumulator
|
||||||
* @param linkPath internal linkPath
|
* @param linkPath internal linkPath
|
||||||
* @return true if no path conflict detected, false if path conflict is detected
|
* @return true if no path conflict detected, false if path conflict is detected
|
||||||
*/
|
*/
|
||||||
private static boolean addLinkPathPath(Set<String> pathSet, String linkPath) {
|
private static boolean addLinkPath(Set<String> pathSet, String linkPath) {
|
||||||
// Must ensure that all paths end with '/' separator - even if path is endpoint
|
// Must ensure that all paths end with '/' separator - even if path is endpoint
|
||||||
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
||||||
linkPath += FileSystem.SEPARATOR;
|
linkPath += FileSystem.SEPARATOR;
|
||||||
|
@ -418,7 +438,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
if (parent instanceof LinkedDomainFolder lf) {
|
if (parent instanceof LinkedDomainFolder lf) {
|
||||||
try {
|
try {
|
||||||
projectData = lf.getLinkedProjectData();
|
projectData = lf.getLinkedProjectData();
|
||||||
addLinkPathPath(linkPathsVisited, lf.getLinkedPathname());
|
addLinkPath(linkPathsVisited, lf.getLinkedPathname());
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
throw new RuntimeException("Unexpected", e);
|
throw new RuntimeException("Unexpected", e);
|
||||||
|
@ -426,7 +446,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
projectData = parent.getProjectData();
|
projectData = parent.getProjectData();
|
||||||
addLinkPathPath(linkPathsVisited, file.getPathname());
|
addLinkPath(linkPathsVisited, file.getPathname());
|
||||||
}
|
}
|
||||||
|
|
||||||
String contentType = file.getContentType();
|
String contentType = file.getContentType();
|
||||||
|
@ -466,19 +486,22 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
return nextLinkFile;
|
return nextLinkFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!addLinkPathPath(linkPathsVisited, linkPath)) {
|
|
||||||
errorConsumer.accept("Link has a circular reference");
|
|
||||||
break; // broken and can't continue
|
|
||||||
}
|
|
||||||
|
|
||||||
DomainFile linkedFile = null;
|
DomainFile linkedFile = null;
|
||||||
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
|
||||||
linkedFile = projectData.getFile(linkPath);
|
linkedFile = projectData.getFile(linkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DomainFolder linkedFolder = null;
|
||||||
if (isFolderLink) {
|
if (isFolderLink) {
|
||||||
// Check for folder existence at linkPath
|
linkedFolder = getNonLinkedFolder(projectData, linkPath);
|
||||||
if (getNonLinkedFolder(projectData, linkPath) != null) {
|
}
|
||||||
|
|
||||||
|
if (linkedFolder == null && !addLinkPath(linkPathsVisited, linkPath)) {
|
||||||
|
errorConsumer.accept("Link has a circular reference");
|
||||||
|
break; // broken and can't continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolderLink && linkedFolder != null) {
|
||||||
// Check for folder-link that conflicts with folder found
|
// Check for folder-link that conflicts with folder found
|
||||||
if (linkedFile != null) {
|
if (linkedFile != null) {
|
||||||
LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo();
|
LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo();
|
||||||
|
@ -492,7 +515,6 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
|
||||||
statusConsumer.accept(LinkStatus.INTERNAL);
|
statusConsumer.accept(LinkStatus.INTERNAL);
|
||||||
return nextLinkFile;
|
return nextLinkFile;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (linkedFile == null) {
|
if (linkedFile == null) {
|
||||||
String acceptableType = isFolderLink ? "folder" : "file";
|
String acceptableType = isFolderLink ? "folder" : "file";
|
||||||
|
|
|
@ -43,21 +43,21 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
private final LinkedGhidraSubFolder parent;
|
private final LinkedGhidraSubFolder parent;
|
||||||
private final String fileName;
|
private final String fileName;
|
||||||
|
private final DomainFile realDomainFile;
|
||||||
|
private final LinkFileInfo linkInfo;
|
||||||
|
|
||||||
LinkedGhidraFile(LinkedGhidraSubFolder parent, String fileName) {
|
LinkedGhidraFile(LinkedGhidraSubFolder parent, DomainFile realDomainFile) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.fileName = fileName;
|
this.fileName = realDomainFile.getName();
|
||||||
|
this.realDomainFile = realDomainFile;
|
||||||
|
this.linkInfo = realDomainFile.isLink() ? new LinkedFileLinkInfo() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile getLinkedFile() throws IOException {
|
public DomainFile getRealFile() throws IOException {
|
||||||
return parent.getLinkedFile(fileName);
|
return parent.getLinkedFile(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DomainFile getLinkedFileNoError() {
|
|
||||||
return parent.getLinkedFileNoError(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFolder getParent() {
|
public DomainFolder getParent() {
|
||||||
return parent;
|
return parent;
|
||||||
|
@ -94,18 +94,18 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists() {
|
public boolean exists() {
|
||||||
return getLinkedFileNoError() != null;
|
DomainFile df = parent.getLinkedFileNoError(fileName);
|
||||||
|
return df != null && df.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFileID() {
|
public String getFileID() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getFileID();
|
||||||
return df != null ? df.getFileID() : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile setName(String newName) throws InvalidNameException, IOException {
|
public DomainFile setName(String newName) throws InvalidNameException, IOException {
|
||||||
String name = getLinkedFile().setName(newName).getName();
|
String name = getRealFile().setName(newName).getName();
|
||||||
return parent.getFile(name);
|
return parent.getFile(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,46 +156,40 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getContentType() {
|
public String getContentType() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getContentType();
|
||||||
return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getDomainObjectClass();
|
||||||
return df != null ? df.getDomainObjectClass() : DomainObject.class;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
|
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
|
||||||
return getLinkedFile().getChangesByOthersSinceCheckout();
|
return getRealFile().getChangesByOthersSinceCheckout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
|
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
|
||||||
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
|
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
|
||||||
return getLinkedFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
|
return getRealFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainObject getOpenedDomainObject(Object consumer) {
|
public DomainObject getOpenedDomainObject(Object consumer) {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getOpenedDomainObject(consumer);
|
||||||
if (df != null) {
|
|
||||||
return df.getOpenedDomainObject(consumer);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
|
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
|
||||||
throws VersionException, IOException, CancelledException {
|
throws VersionException, IOException, CancelledException {
|
||||||
return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor);
|
return getRealFile().getReadOnlyDomainObject(consumer, version, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
|
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
|
||||||
throws VersionException, IOException, CancelledException {
|
throws VersionException, IOException, CancelledException {
|
||||||
return getLinkedFile().getImmutableDomainObject(consumer, version, monitor);
|
return getRealFile().getImmutableDomainObject(consumer, version, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -226,191 +220,174 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getLastModifiedTime() {
|
public long getLastModifiedTime() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getLastModifiedTime();
|
||||||
return df != null ? df.getLastModifiedTime() : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Icon getIcon(boolean disabled) {
|
public Icon getIcon(boolean disabled) {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getIcon(disabled);
|
||||||
return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCheckedOut() {
|
public boolean isCheckedOut() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isCheckedOut();
|
||||||
return df != null ? df.isCheckedOut() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCheckedOutExclusive() {
|
public boolean isCheckedOutExclusive() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isCheckedOutExclusive();
|
||||||
return df != null ? df.isCheckedOutExclusive() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean modifiedSinceCheckout() {
|
public boolean modifiedSinceCheckout() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.modifiedSinceCheckout();
|
||||||
return df != null ? df.modifiedSinceCheckout() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canCheckout() {
|
public boolean canCheckout() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.canCheckout();
|
||||||
return df != null ? df.canCheckout() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canCheckin() {
|
public boolean canCheckin() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.canCheckin();
|
||||||
return df != null ? df.canCheckin() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canMerge() {
|
public boolean canMerge() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.canMerge();
|
||||||
return df != null ? df.canMerge() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canAddToRepository() {
|
public boolean canAddToRepository() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.canAddToRepository();
|
||||||
return df != null ? df.canAddToRepository() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setReadOnly(boolean state) throws IOException {
|
public void setReadOnly(boolean state) throws IOException {
|
||||||
getLinkedFile().setReadOnly(state);
|
getRealFile().setReadOnly(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReadOnly() {
|
public boolean isReadOnly() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isReadOnly();
|
||||||
// read-only state not reflected by icon
|
|
||||||
return df != null ? df.isReadOnly() : true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isVersioned() {
|
public boolean isVersioned() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isVersioned();
|
||||||
return df != null ? df.isVersioned() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isHijacked() {
|
public boolean isHijacked() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isHijacked();
|
||||||
return df != null ? df.isHijacked() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getLatestVersion() {
|
public int getLatestVersion() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getLatestVersion();
|
||||||
return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLatestVersion() {
|
public boolean isLatestVersion() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isLatestVersion();
|
||||||
return df != null ? df.isLatestVersion() : true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getVersion() {
|
public int getVersion() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getVersion();
|
||||||
return df != null ? df.getVersion() : DomainFile.DEFAULT_VERSION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Version[] getVersionHistory() throws IOException {
|
public Version[] getVersionHistory() throws IOException {
|
||||||
DomainFile df = getLinkedFile();
|
DomainFile df = getRealFile();
|
||||||
return df != null ? df.getVersionHistory() : new Version[0];
|
return df != null ? df.getVersionHistory() : new Version[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
|
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException {
|
throws IOException, CancelledException {
|
||||||
getLinkedFile().addToVersionControl(comment, keepCheckedOut, monitor);
|
getRealFile().addToVersionControl(comment, keepCheckedOut, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkout(boolean exclusive, TaskMonitor monitor)
|
public boolean checkout(boolean exclusive, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException {
|
throws IOException, CancelledException {
|
||||||
return getLinkedFile().checkout(exclusive, monitor);
|
return getRealFile().checkout(exclusive, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor)
|
public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor)
|
||||||
throws IOException, VersionException, CancelledException {
|
throws IOException, VersionException, CancelledException {
|
||||||
getLinkedFile().checkin(checkinHandler, monitor);
|
getRealFile().checkin(checkinHandler, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void merge(boolean okToUpgrade, TaskMonitor monitor)
|
public void merge(boolean okToUpgrade, TaskMonitor monitor)
|
||||||
throws IOException, VersionException, CancelledException {
|
throws IOException, VersionException, CancelledException {
|
||||||
getLinkedFile().merge(okToUpgrade, monitor);
|
getRealFile().merge(okToUpgrade, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void undoCheckout(boolean keep) throws IOException {
|
public void undoCheckout(boolean keep) throws IOException {
|
||||||
getLinkedFile().undoCheckout(keep);
|
getRealFile().undoCheckout(keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void undoCheckout(boolean keep, boolean force) throws IOException {
|
public void undoCheckout(boolean keep, boolean force) throws IOException {
|
||||||
getLinkedFile().undoCheckout(keep, force);
|
getRealFile().undoCheckout(keep, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void terminateCheckout(long checkoutId) throws IOException {
|
public void terminateCheckout(long checkoutId) throws IOException {
|
||||||
getLinkedFile().terminateCheckout(checkoutId);
|
getRealFile().terminateCheckout(checkoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ItemCheckoutStatus[] getCheckouts() throws IOException {
|
public ItemCheckoutStatus[] getCheckouts() throws IOException {
|
||||||
return getLinkedFile().getCheckouts();
|
return getRealFile().getCheckouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ItemCheckoutStatus getCheckoutStatus() throws IOException {
|
public ItemCheckoutStatus getCheckoutStatus() throws IOException {
|
||||||
return getLinkedFile().getCheckoutStatus();
|
return getRealFile().getCheckoutStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete() throws IOException {
|
public void delete() throws IOException {
|
||||||
getLinkedFile().delete();
|
getRealFile().delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(int version) throws IOException {
|
public void delete(int version) throws IOException {
|
||||||
getLinkedFile().delete(version);
|
getRealFile().delete(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile moveTo(DomainFolder newParent) throws IOException {
|
public DomainFile moveTo(DomainFolder newParent) throws IOException {
|
||||||
return getLinkedFile().moveTo(newParent);
|
return getRealFile().moveTo(newParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
|
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException {
|
throws IOException, CancelledException {
|
||||||
return getLinkedFile().copyTo(newParent, monitor);
|
return getRealFile().copyTo(newParent, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
|
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException {
|
throws IOException, CancelledException {
|
||||||
return getLinkedFile().copyVersionTo(version, destFolder, monitor);
|
return getRealFile().copyVersionTo(version, destFolder, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
|
public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
|
||||||
return getLinkedFile().copyToAsLink(newParent, relative);
|
return getRealFile().copyToAsLink(newParent, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLinkingSupported() {
|
public boolean isLinkingSupported() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isLinkingSupported();
|
||||||
return df != null ? df.isLinkingSupported() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -420,8 +397,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isChanged() {
|
public boolean isChanged() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.isChanged();
|
||||||
return df != null ? df.isChanged() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -436,31 +412,57 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
|
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
|
||||||
getLinkedFile().packFile(file, monitor);
|
getRealFile().packFile(file, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getMetadata() {
|
public Map<String, String> getMetadata() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.getMetadata();
|
||||||
return df != null ? df.getMetadata() : Map.of();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long length() throws IOException {
|
public long length() throws IOException {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return realDomainFile.length();
|
||||||
return df != null ? df.length() : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLink() {
|
public boolean isLink() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return linkInfo != null;
|
||||||
return df != null ? df.isLink() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LinkFileInfo getLinkInfo() {
|
public LinkFileInfo getLinkInfo() {
|
||||||
DomainFile df = getLinkedFileNoError();
|
return linkInfo;
|
||||||
return df != null ? df.getLinkInfo() : null;
|
}
|
||||||
|
|
||||||
|
private class LinkedFileLinkInfo implements LinkFileInfo {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DomainFile getFile() {
|
||||||
|
return LinkedGhidraFile.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinkedGhidraFolder getLinkedFolder() {
|
||||||
|
try {
|
||||||
|
return FolderLinkContentHandler.getLinkedFolder(LinkedGhidraFile.this);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLinkPath() {
|
||||||
|
return realDomainFile.getLinkInfo().getLinkPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAbsoluteLinkPath() throws IOException {
|
||||||
|
return realDomainFile.getLinkInfo().getAbsoluteLinkPath();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -470,12 +472,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
String str = parent.toString();
|
return getPathname() + "->" + realDomainFile.getPathname();
|
||||||
if (!str.endsWith("/")) {
|
|
||||||
str += "/";
|
|
||||||
}
|
|
||||||
str += getName();
|
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
||||||
* since an error will occur if there is no active project at the time the link is followed.
|
* since an error will occur if there is no active project at the time the link is followed.
|
||||||
*
|
*
|
||||||
* @param folderLinkFile link-file which corresponds to a linked-folder
|
* @param folderLinkFile link-file which corresponds to a linked-folder
|
||||||
* (see {@link LinkFileInfo#isFolderLink()}).
|
* (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
|
||||||
|
* of this linked-folder.
|
||||||
* @param linkedFolderUrl linked folder URL
|
* @param linkedFolderUrl linked folder URL
|
||||||
*/
|
*/
|
||||||
LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
|
LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
|
||||||
|
@ -87,7 +88,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
||||||
* {@link #getProjectData() project data} instance.
|
* {@link #getProjectData() project data} instance.
|
||||||
*
|
*
|
||||||
* @param folderLinkFile link-file which corresponds to a linked-folder
|
* @param folderLinkFile link-file which corresponds to a linked-folder
|
||||||
* (see {@link LinkFileInfo#isFolderLink()}).
|
* (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
|
||||||
|
* of this linked-folder.
|
||||||
* @param linkedFolder locally-linked folder within same project
|
* @param linkedFolder locally-linked folder within same project
|
||||||
*/
|
*/
|
||||||
LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
|
LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
|
||||||
|
@ -114,7 +116,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
||||||
if (!(obj instanceof LinkedGhidraFolder other)) {
|
if (!(obj instanceof LinkedGhidraFolder other)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return linkedPathname.equals(other.linkedPathname) &&
|
return getPathname().equals(other.getPathname()) &&
|
||||||
folderLinkFile.equals(other.folderLinkFile);
|
folderLinkFile.equals(other.folderLinkFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,9 +240,9 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (linkedFolder != null) {
|
if (linkedFolder != null) {
|
||||||
return "->" + getLinkedPathname();
|
return getPathname() + "->" + getLinkedPathname();
|
||||||
}
|
}
|
||||||
return "->" + linkedFolderUrl.toString();
|
return getPathname() + "->" + linkedFolderUrl.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -274,7 +274,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
||||||
DomainFile[] files = linkedFolder.getFiles();
|
DomainFile[] files = linkedFolder.getFiles();
|
||||||
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
|
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
|
||||||
for (int i = 0; i < files.length; i++) {
|
for (int i = 0; i < files.length; i++) {
|
||||||
linkedSubFolders[i] = new LinkedGhidraFile(this, files[i].getName());
|
linkedSubFolders[i] = new LinkedGhidraFile(this, files[i]);
|
||||||
}
|
}
|
||||||
return linkedSubFolders;
|
return linkedSubFolders;
|
||||||
}
|
}
|
||||||
|
@ -286,6 +286,10 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the true file within this linked folder.
|
* Get the true file within this linked folder.
|
||||||
|
* <P>
|
||||||
|
* NOTE: The returned file is the "real" file and would be the have the equivalence:
|
||||||
|
* {@code folder.getLinkedFileNoError("X") == folder.getFile("X").getRealFile() }.
|
||||||
|
*
|
||||||
* @param name file name
|
* @param name file name
|
||||||
* @return file or null if not found or error occurs
|
* @return file or null if not found or error occurs
|
||||||
*/
|
*/
|
||||||
|
@ -300,6 +304,16 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the true file within this linked folder.
|
||||||
|
* <P>
|
||||||
|
* NOTE: The returned file is the "real" file and would be the have the equivalence:
|
||||||
|
* {@code folder.getLinkedFile("X") == folder.getFile("X").getRealFile() }.
|
||||||
|
*
|
||||||
|
* @param name file name
|
||||||
|
* @return file or null if not found or error occurs
|
||||||
|
* @throws IOException if an IO error ocurs such as FileNotFoundException
|
||||||
|
*/
|
||||||
DomainFile getLinkedFile(String name) throws IOException {
|
DomainFile getLinkedFile(String name) throws IOException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
DomainFile df = linkedFolder.getFile(name);
|
DomainFile df = linkedFolder.getFile(name);
|
||||||
|
@ -311,8 +325,8 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile getFile(String name) {
|
public DomainFile getFile(String name) {
|
||||||
DomainFile f = getLinkedFileNoError(name);
|
DomainFile file = getLinkedFileNoError(name);
|
||||||
return f != null ? new LinkedGhidraFile(this, name) : null;
|
return file != null ? new LinkedGhidraFile(this, file) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -333,36 +347,40 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
|
||||||
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
|
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
|
||||||
throws InvalidNameException, IOException, CancelledException {
|
throws InvalidNameException, IOException, CancelledException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
return linkedFolder.createFile(name, obj, monitor);
|
DomainFile file = linkedFolder.createFile(name, obj, monitor);
|
||||||
|
return getFile(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
|
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
|
||||||
throws InvalidNameException, IOException, CancelledException {
|
throws InvalidNameException, IOException, CancelledException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
return linkedFolder.createFile(name, packFile, monitor);
|
DomainFile file = linkedFolder.createFile(name, packFile, monitor);
|
||||||
|
return getFile(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
|
public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
|
||||||
boolean makeRelative, String linkFilename, LinkHandler<?> lh) throws IOException {
|
boolean makeRelative, String linkFilename, LinkHandler<?> lh) throws IOException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename,
|
DomainFile file = linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative,
|
||||||
lh);
|
linkFilename, lh);
|
||||||
|
return getFile(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh)
|
public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
|
DomainFile file = linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
|
||||||
|
return getFile(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
|
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
|
||||||
DomainFolder linkedFolder = getRealFolder();
|
DomainFolder linkedFolder = getRealFolder();
|
||||||
DomainFolder child = linkedFolder.createFolder(name);
|
DomainFolder folder = linkedFolder.createFolder(name);
|
||||||
return new LinkedGhidraSubFolder(parent, child.getName());
|
return getFolder(folder.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -15,11 +15,26 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.framework.data;
|
package ghidra.framework.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
import ghidra.framework.model.DomainFolderChangeListener;
|
import ghidra.framework.model.DomainFolderChangeListener;
|
||||||
import ghidra.framework.store.FileSystem;
|
import ghidra.framework.store.FileSystem;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class RootGhidraFolderData extends GhidraFolderData {
|
public class RootGhidraFolderData extends GhidraFolderData {
|
||||||
|
|
||||||
|
// Folder path reference counts, associated with discovered file-links and folder-links,
|
||||||
|
// are tracked to ensure that such folders are visited immediately or upon their
|
||||||
|
// creation to ensure that the folder change listener is properly notified of all changes
|
||||||
|
// related to the folder paths contained within this map.
|
||||||
|
private HashMap<String, Integer> folderReferenceCounts = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for project data root folder.
|
||||||
|
* @param projectData project data
|
||||||
|
* @param listener folder change listener
|
||||||
|
*/
|
||||||
RootGhidraFolderData(DefaultProjectData projectData, DomainFolderChangeListener listener) {
|
RootGhidraFolderData(DefaultProjectData projectData, DomainFolderChangeListener listener) {
|
||||||
super(projectData, listener);
|
super(projectData, listener);
|
||||||
}
|
}
|
||||||
|
@ -50,4 +65,97 @@ public class RootGhidraFolderData extends GhidraFolderData {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the specified folder path must be visited due to
|
||||||
|
* possible link references to the folder or one of its children.
|
||||||
|
* @param folderPathname folder pathname (not ending with '/')
|
||||||
|
* @return true if folder should be visited to ensure that changes are properly tracked
|
||||||
|
* with proper change notifications sent.
|
||||||
|
*/
|
||||||
|
public boolean mustVisit(String folderPathname) {
|
||||||
|
return folderReferenceCounts.containsKey(folderPathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register internal file/folder-link to ensure we do not ignore change events which affect
|
||||||
|
* the referenced file/folder.
|
||||||
|
* @param absoluteLinkPath absolute internal path referenced by a link-file
|
||||||
|
*/
|
||||||
|
void registerInternalLinkPath(String absoluteLinkPath) {
|
||||||
|
if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register path elements upto parent of absoluteLinkPath
|
||||||
|
String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
|
||||||
|
int folderElementCount = pathSplit.length - 1;
|
||||||
|
|
||||||
|
// Start at 1 since element 0 corresponds to root and will be empty
|
||||||
|
GhidraFolderData folderData = this;
|
||||||
|
StringBuilder pathBuilder = new StringBuilder();
|
||||||
|
for (int i = 1; i < folderElementCount; i++) {
|
||||||
|
String folderName = pathSplit[i];
|
||||||
|
if (folderName.length() == 0) {
|
||||||
|
// ignore blank names
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (folderData != null) {
|
||||||
|
folderData = folderData.getFolderData(folderName, false);
|
||||||
|
if (folderData != null && !folderData.visited()) {
|
||||||
|
try {
|
||||||
|
folderData.refresh(false, true, TaskMonitor.DUMMY);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// ignore - things may get out-of-sync
|
||||||
|
folderData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment folder reference count for all folders leading up to referenced folder
|
||||||
|
pathBuilder.append(FileSystem.SEPARATOR);
|
||||||
|
pathBuilder.append(folderName);
|
||||||
|
folderReferenceCounts.compute(pathBuilder.toString(),
|
||||||
|
(path, count) -> (count == null) ? 1 : ++count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister internal file/folder-link to ensure we do not ignore change events which affect
|
||||||
|
* the referenced file/folder.
|
||||||
|
* @param absoluteLinkPath absolute internal path referenced by a link-file
|
||||||
|
*/
|
||||||
|
void unregisterInternalLinkPath(String absoluteLinkPath) {
|
||||||
|
if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register path elements upto parent of absoluteLinkPath
|
||||||
|
String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
|
||||||
|
int folderElementCount = pathSplit.length - 1;
|
||||||
|
|
||||||
|
// Start at 1 since element 0 corresponds to root and will be empty
|
||||||
|
StringBuilder pathBuilder = new StringBuilder();
|
||||||
|
for (int i = 1; i < folderElementCount; i++) {
|
||||||
|
String folderName = pathSplit[i];
|
||||||
|
if (folderName.length() == 0) {
|
||||||
|
// ignore blank names
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Increment folder reference count for all folders leading up to referenced folder
|
||||||
|
pathBuilder.append(FileSystem.SEPARATOR);
|
||||||
|
pathBuilder.append(folderName);
|
||||||
|
String path = pathBuilder.toString();
|
||||||
|
Integer count = folderReferenceCounts.get(path);
|
||||||
|
if (count != null) {
|
||||||
|
if (count == 1) {
|
||||||
|
folderReferenceCounts.remove(path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
folderReferenceCounts.put(path, count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
|
||||||
import ghidra.framework.preferences.Preferences;
|
import ghidra.framework.preferences.Preferences;
|
||||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||||
import ghidra.framework.remote.User;
|
import ghidra.framework.remote.User;
|
||||||
|
import ghidra.framework.store.FileSystem;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.filechooser.GhidraFileChooserModel;
|
import ghidra.util.filechooser.GhidraFileChooserModel;
|
||||||
import ghidra.util.filechooser.GhidraFileFilter;
|
import ghidra.util.filechooser.GhidraFileFilter;
|
||||||
|
@ -137,6 +138,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
private ProjectDataRenameAction renameAction;
|
private ProjectDataRenameAction renameAction;
|
||||||
private ProjectDataOpenDefaultToolAction openAction;
|
private ProjectDataOpenDefaultToolAction openAction;
|
||||||
private ProjectDataFollowLinkAction followLinkAction;
|
private ProjectDataFollowLinkAction followLinkAction;
|
||||||
|
private ProjectDataSelectRealFileOrFolderAction selectRealFileOrFolderAction;
|
||||||
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
|
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
|
||||||
private ProjectDataCollapseAction<FrontEndProjectTreeContext> collapseAction;
|
private ProjectDataCollapseAction<FrontEndProjectTreeContext> collapseAction;
|
||||||
private ProjectDataSelectAction selectAction;
|
private ProjectDataSelectAction selectAction;
|
||||||
|
@ -221,6 +223,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
// Top of popup menu actions - no group
|
// Top of popup menu actions - no group
|
||||||
openAction = new ProjectDataOpenDefaultToolAction(owner, null);
|
openAction = new ProjectDataOpenDefaultToolAction(owner, null);
|
||||||
followLinkAction = new ProjectDataFollowLinkAction(this, null);
|
followLinkAction = new ProjectDataFollowLinkAction(this, null);
|
||||||
|
selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
|
||||||
|
|
||||||
String groupName = "Cut/copy/paste/new1";
|
String groupName = "Cut/copy/paste/new1";
|
||||||
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
|
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
|
||||||
|
@ -258,6 +261,7 @@ public class FrontEndPlugin extends Plugin
|
||||||
tool.addAction(deleteAction);
|
tool.addAction(deleteAction);
|
||||||
tool.addAction(openAction);
|
tool.addAction(openAction);
|
||||||
tool.addAction(followLinkAction);
|
tool.addAction(followLinkAction);
|
||||||
|
tool.addAction(selectRealFileOrFolderAction);
|
||||||
tool.addAction(renameAction);
|
tool.addAction(renameAction);
|
||||||
tool.addAction(expandAction);
|
tool.addAction(expandAction);
|
||||||
tool.addAction(collapseAction);
|
tool.addAction(collapseAction);
|
||||||
|
@ -1117,11 +1121,17 @@ public class FrontEndPlugin extends Plugin
|
||||||
showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
|
showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
|
||||||
}
|
}
|
||||||
else if (!dataTreePanel.isShowing()) {
|
else if (!dataTreePanel.isShowing()) {
|
||||||
// Filter table on absolute link path
|
|
||||||
String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
|
String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
|
||||||
|
if (linkPath.startsWith(FileSystem.SEPARATOR) && linkPath.length() > 1) {
|
||||||
|
// Filter table on absolute internal link path
|
||||||
|
if (linkPath.endsWith(FileSystem.SEPARATOR)) {
|
||||||
|
// Remove trailing '/' from path to ensure we match
|
||||||
|
linkPath = linkPath.substring(0, linkPath.length() - 1);
|
||||||
|
}
|
||||||
dataTablePanel.setFilter(linkPath);
|
dataTablePanel.setFilter(linkPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage());
|
Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package ghidra.framework.main;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import docking.action.MenuData;
|
||||||
|
import ghidra.framework.main.datatable.FrontendProjectTreeAction;
|
||||||
|
import ghidra.framework.main.datatable.ProjectDataContext;
|
||||||
|
import ghidra.framework.main.datatree.DataTree;
|
||||||
|
import ghidra.framework.model.*;
|
||||||
|
import ghidra.util.HelpLocation;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
|
public class ProjectDataSelectRealFileOrFolderAction extends FrontendProjectTreeAction {
|
||||||
|
|
||||||
|
private FrontEndPlugin plugin;
|
||||||
|
|
||||||
|
public ProjectDataSelectRealFileOrFolderAction(FrontEndPlugin plugin, String group) {
|
||||||
|
super("Select Real File or Folder", plugin.getName());
|
||||||
|
this.plugin = plugin;
|
||||||
|
setPopupMenuData(new MenuData(new String[] { "Select Real File" }, group));
|
||||||
|
setHelpLocation(new HelpLocation("FrontEndPlugin", "Select_Real_File_or_Folder"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void actionPerformed(ProjectDataContext context) {
|
||||||
|
|
||||||
|
boolean isFolder = false;
|
||||||
|
String pathname;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
|
||||||
|
DomainFolder folder = context.getSelectedFolders().get(0);
|
||||||
|
if (!(folder instanceof LinkedDomainFolder linkedFolder)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isFolder = true;
|
||||||
|
pathname = linkedFolder.getRealFolder().getPathname();
|
||||||
|
}
|
||||||
|
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
|
||||||
|
DomainFile file = context.getSelectedFiles().get(0);
|
||||||
|
if (!(file instanceof LinkedDomainFile linkedFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isFolder = false;
|
||||||
|
pathname = linkedFile.getRealFile().getPathname();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path is local to its project data tree
|
||||||
|
plugin.showInProjectTree(context.getProjectData(), pathname, isFolder);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
Msg.showError(this, null, "Linked Content Error",
|
||||||
|
"Failed to resolve linked " + (isFolder ? "folder" : "file"), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isEnabledForContext(ProjectDataContext context) {
|
||||||
|
boolean enabled = false;
|
||||||
|
String contentType = "Content";
|
||||||
|
if (context.getComponent() instanceof DataTree) {
|
||||||
|
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
|
||||||
|
DomainFolder folder = context.getSelectedFolders().get(0);
|
||||||
|
if (folder instanceof LinkedDomainFolder) {
|
||||||
|
contentType = "Folder";
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
|
||||||
|
DomainFile file = context.getSelectedFiles().get(0);
|
||||||
|
if (file instanceof LinkedDomainFile) {
|
||||||
|
contentType = "File";
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enabled) {
|
||||||
|
setPopupMenuData(new MenuData(new String[] { "Select Real " + contentType },
|
||||||
|
getPopupMenuData().getMenuGroup()));
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,9 @@ import docking.widgets.tree.GTreeNode;
|
||||||
import ghidra.framework.data.LinkHandler;
|
import ghidra.framework.data.LinkHandler;
|
||||||
import ghidra.framework.main.datatree.DataTreeNode.NodeType;
|
import ghidra.framework.main.datatree.DataTreeNode.NodeType;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
|
import ghidra.framework.store.local.LocalFileSystem;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to handle changes when a domain folder changes; updates the
|
* Class to handle changes when a domain folder changes; updates the
|
||||||
|
@ -49,6 +52,10 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only
|
private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only
|
||||||
|
|
||||||
|
// The refreshedTrackingSet is used to track recursive path refreshes to avoid infinite
|
||||||
|
// recursion. See updateLinkedContent and LinkedTreeNode.refreshLinks methods.
|
||||||
|
private HashSet<String> refreshedTrackingSet;
|
||||||
|
|
||||||
ChangeManager(ProjectDataTreePanel treePanel) {
|
ChangeManager(ProjectDataTreePanel treePanel) {
|
||||||
this.treePanel = treePanel;
|
this.treePanel = treePanel;
|
||||||
projectData = treePanel.getProjectData();
|
projectData = treePanel.getProjectData();
|
||||||
|
@ -75,11 +82,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFileAdded(DomainFile file) {
|
public void domainFileAdded(DomainFile file) {
|
||||||
|
|
||||||
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
||||||
String fileName = file.getName();
|
String fileName = file.getName();
|
||||||
DomainFolder parentFolder = file.getParent();
|
DomainFolder parentFolder = file.getParent();
|
||||||
updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink),
|
updateLinkedContent(parentFolder.getPathname(), p -> addFileNode(p, fileName, isFolderLink),
|
||||||
ltn -> ltn.refreshLinks(fileName));
|
ltn -> ltn.refreshLinks(fileName));
|
||||||
|
|
||||||
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
||||||
if (folderNode != null && folderNode.isLoaded()) {
|
if (folderNode != null && folderNode.isLoaded()) {
|
||||||
addFileNode(folderNode, fileName, isFolderLink);
|
addFileNode(folderNode, fileName, isFolderLink);
|
||||||
|
@ -88,7 +97,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
|
public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
|
||||||
updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
|
|
||||||
|
updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
|
||||||
|
|
||||||
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
|
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
|
||||||
if (folderNode != null) {
|
if (folderNode != null) {
|
||||||
updateChildren(folderNode);
|
updateChildren(folderNode);
|
||||||
|
@ -97,14 +108,16 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFileRenamed(DomainFile file, String oldName) {
|
public void domainFileRenamed(DomainFile file, String oldName) {
|
||||||
|
|
||||||
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
|
||||||
updateLinkedContent(file.getParent(), p -> {
|
updateLinkedContent(file.getParent().getPathname(), p -> {
|
||||||
updateChildren(p);
|
updateChildren(p);
|
||||||
addFileNode(p, file.getName(), isFolderLink);
|
addFileNode(p, file.getName(), isFolderLink);
|
||||||
}, ltn -> {
|
}, ltn -> {
|
||||||
ltn.refreshLinks(oldName);
|
ltn.refreshLinks(oldName);
|
||||||
ltn.refreshLinks(file.getName());
|
ltn.refreshLinks(file.getName());
|
||||||
});
|
});
|
||||||
|
|
||||||
DomainFolder parent = file.getParent();
|
DomainFolder parent = file.getParent();
|
||||||
skipLinkUpdate = true;
|
skipLinkUpdate = true;
|
||||||
try {
|
try {
|
||||||
|
@ -124,10 +137,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
|
public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
|
||||||
|
|
||||||
|
LinkFileInfo linkInfo = file.getLinkInfo();
|
||||||
|
boolean isFolderLink = linkInfo != null && linkInfo.isFolderLink();
|
||||||
|
|
||||||
DomainFolder parentFolder = file.getParent();
|
DomainFolder parentFolder = file.getParent();
|
||||||
updateLinkedContent(parentFolder, fn -> {
|
updateLinkedContent(parentFolder.getPathname(), fn -> {
|
||||||
/* No folder update required */
|
// Refresh any linked folder content containing file
|
||||||
|
if (fn.isLoaded()) {
|
||||||
|
NodeType type = isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE;
|
||||||
|
DomainFileNode fileNode = (DomainFileNode) fn.getChild(file.getName(), type);
|
||||||
|
if (fileNode != null) {
|
||||||
|
fileNode.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
}, ltn -> ltn.refreshLinks(file.getName()));
|
}, ltn -> ltn.refreshLinks(file.getName()));
|
||||||
|
|
||||||
DomainFileNode fileNode = findDomainFileNode(file, true);
|
DomainFileNode fileNode = findDomainFileNode(file, true);
|
||||||
if (fileNode != null) {
|
if (fileNode != null) {
|
||||||
fileNode.refresh();
|
fileNode.refresh();
|
||||||
|
@ -141,10 +166,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFolderAdded(DomainFolder folder) {
|
public void domainFolderAdded(DomainFolder folder) {
|
||||||
|
|
||||||
String folderName = folder.getName();
|
String folderName = folder.getName();
|
||||||
DomainFolder parentFolder = folder.getParent();
|
DomainFolder parentFolder = folder.getParent();
|
||||||
updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName),
|
updateLinkedContent(parentFolder.getPathname(), p -> addFolderNode(p, folderName),
|
||||||
ltn -> ltn.refreshLinks(folderName));
|
ltn -> ltn.refreshLinks(folderName));
|
||||||
|
|
||||||
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
|
||||||
if (folderNode != null && folderNode.isLoaded()) {
|
if (folderNode != null && folderNode.isLoaded()) {
|
||||||
addFolderNode(folderNode, folderName);
|
addFolderNode(folderNode, folderName);
|
||||||
|
@ -153,7 +180,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFolderRemoved(DomainFolder parent, String name) {
|
public void domainFolderRemoved(DomainFolder parent, String name) {
|
||||||
updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
|
|
||||||
|
updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
|
||||||
|
|
||||||
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
|
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
|
||||||
if (folderNode != null) {
|
if (folderNode != null) {
|
||||||
updateChildren(folderNode);
|
updateChildren(folderNode);
|
||||||
|
@ -162,17 +191,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFolderRenamed(DomainFolder folder, String oldName) {
|
public void domainFolderRenamed(DomainFolder folder, String oldName) {
|
||||||
updateLinkedContent(folder.getParent(), p -> {
|
|
||||||
updateChildren(p);
|
domainFolderMoved(folder.getParent().getPathname(), oldName, folder);
|
||||||
addFolderNode(p, folder.getName());
|
|
||||||
}, ltn -> {
|
|
||||||
ltn.refreshLinks(oldName);
|
|
||||||
ltn.refreshLinks(folder.getName());
|
|
||||||
});
|
|
||||||
DomainFolder parent = folder.getParent();
|
|
||||||
skipLinkUpdate = true;
|
skipLinkUpdate = true;
|
||||||
try {
|
try {
|
||||||
domainFolderRemoved(parent, oldName);
|
domainFolderRemoved(folder.getParent(), oldName);
|
||||||
domainFolderAdded(folder);
|
domainFolderAdded(folder);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
@ -182,9 +206,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) {
|
public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) {
|
||||||
|
|
||||||
|
domainFolderMoved(oldParent.getPathname(), folder.getName(), folder);
|
||||||
|
|
||||||
|
skipLinkUpdate = true;
|
||||||
|
try {
|
||||||
domainFolderRemoved(oldParent, folder.getName());
|
domainFolderRemoved(oldParent, folder.getName());
|
||||||
domainFolderAdded(folder);
|
domainFolderAdded(folder);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
skipLinkUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void domainFolderSetActive(DomainFolder folder) {
|
public void domainFolderSetActive(DomainFolder folder) {
|
||||||
|
@ -194,6 +227,36 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Following a folder move or rename where only a single notification is provided this
|
||||||
|
* method should be used to propogate link related updates which may refer to the affected
|
||||||
|
* folder or its children. This method is invoked recursively for all child folders.
|
||||||
|
* @param oldParentPath folder's old parent path
|
||||||
|
* @param oldName folder's previous name
|
||||||
|
* @param folder folder instance following rename
|
||||||
|
*/
|
||||||
|
private void domainFolderMoved(String oldParentPath, String oldName, DomainFolder folder) {
|
||||||
|
|
||||||
|
String oldFolderPathname = LocalFileSystem.getPath(oldParentPath, oldName);
|
||||||
|
|
||||||
|
// Recurse over all child folders.
|
||||||
|
for (DomainFolder childFolder : folder.getFolders()) {
|
||||||
|
domainFolderMoved(oldFolderPathname, childFolder.getName(), childFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh links to old placement
|
||||||
|
updateLinkedContent(oldParentPath, null, ltn -> {
|
||||||
|
ltn.refreshLinks(oldName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh links to new placement
|
||||||
|
String newName = folder.getName();
|
||||||
|
updateLinkedContent(folder.getParent().getPathname(), p -> addFolderNode(p, newName),
|
||||||
|
ltn -> {
|
||||||
|
ltn.refreshLinks(newName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helper methods
|
// Helper methods
|
||||||
//
|
//
|
||||||
|
@ -210,9 +273,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
|
private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
|
||||||
|
|
||||||
|
Msg.debug(this, "addFileNode: " + node.getPathname() + " " + fileName + " " +
|
||||||
|
Boolean.toString(isFolderLink));
|
||||||
if (node.isLeaf() || !node.isLoaded()) {
|
if (node.isLeaf() || !node.isLoaded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existance of file by that name
|
// Check for existance of file by that name
|
||||||
DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
|
DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
|
||||||
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
||||||
|
@ -234,6 +301,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
if (node.isLeaf() || !node.isLoaded()) {
|
if (node.isLeaf() || !node.isLoaded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existance of folder by that name
|
// Check for existance of folder by that name
|
||||||
if (node.getChild(folderName, NodeType.FOLDER) != null) {
|
if (node.getChild(folderName, NodeType.FOLDER) != null) {
|
||||||
return;
|
return;
|
||||||
|
@ -262,6 +330,20 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
return findDomainFolderNode(folderPath, lazy);
|
return findDomainFolderNode(folderPath, lazy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private List<String> getPathAsList(String pathname) {
|
||||||
|
// ArrayList<String> folderPath = new ArrayList<String>();
|
||||||
|
// String[] pathSplit = pathname.split(FileSystem.SEPARATOR);
|
||||||
|
// for (int i = 1; i < pathSplit.length; i++) {
|
||||||
|
// folderPath.add(pathSplit[i]);
|
||||||
|
// }
|
||||||
|
// return folderPath;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private DomainFolderNode findDomainFolderNode(String pathname, boolean lazy) {
|
||||||
|
// List<String> folderPath = getPathAsList(pathname);
|
||||||
|
// return findDomainFolderNode(folderPath, lazy);
|
||||||
|
// }
|
||||||
|
|
||||||
private DomainFolderNode findDomainFolderNode(List<String> folderPath, boolean lazy) {
|
private DomainFolderNode findDomainFolderNode(List<String> folderPath, boolean lazy) {
|
||||||
DomainFolderNode folderNode = root;
|
DomainFolderNode folderNode = root;
|
||||||
for (String name : folderPath) {
|
for (String name : folderPath) {
|
||||||
|
@ -284,6 +366,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
if (lazy && !folderNode.isLoaded()) {
|
if (lazy && !folderNode.isLoaded()) {
|
||||||
return null; // not visited
|
return null; // not visited
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
|
boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
|
||||||
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
|
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
|
||||||
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
|
||||||
|
@ -334,40 +417,51 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
@Override
|
@Override
|
||||||
public void treeStructureChanged(TreeModelEvent e) {
|
public void treeStructureChanged(TreeModelEvent e) {
|
||||||
|
|
||||||
// This is used when an existing node is loaded to register all of its link-file children
|
// NOTE: We have seen getTreePath return null in the test environment
|
||||||
// since the occurance of treeNodesChanged cannot be relied upon for notification of
|
// immediately before ChangeManager disposal
|
||||||
// these existing children.
|
|
||||||
|
|
||||||
TreePath treePath = e.getTreePath();
|
TreePath treePath = e.getTreePath();
|
||||||
if (treePath == null) {
|
|
||||||
return;
|
Object[] changedChildren = e.getChildren();
|
||||||
|
if (changedChildren != null) {
|
||||||
|
for (Object child : changedChildren) {
|
||||||
|
treeNodeChanged(child, true);
|
||||||
}
|
}
|
||||||
Object treeNode = treePath.getLastPathComponent();
|
}
|
||||||
|
else if (treePath != null) {
|
||||||
|
treeNodeChanged(treePath.getLastPathComponent(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void treeNodeChanged(Object treeNode, boolean processLoadedChildren) {
|
||||||
|
|
||||||
if (!(treeNode instanceof DataTreeNode dataTreeNode)) {
|
if (!(treeNode instanceof DataTreeNode dataTreeNode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!dataTreeNode.isLoaded()) {
|
|
||||||
return;
|
if (treeNode instanceof DomainFileNode fileNode) {
|
||||||
}
|
|
||||||
// Register all visible link-file nodes
|
|
||||||
for (GTreeNode child : dataTreeNode.getChildren()) {
|
|
||||||
if (child instanceof DomainFileNode fileNode) {
|
|
||||||
if (fileNode.getDomainFile().isLink()) {
|
|
||||||
addLinkFile(fileNode);
|
addLinkFile(fileNode);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// TODO: Not sure we need the following code
|
||||||
|
// if (processLoadedChildren && dataTreeNode.isLoaded()) {
|
||||||
|
// for (GTreeNode node : dataTreeNode.getChildren()) {
|
||||||
|
// treeNodeChanged(node, true);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void treeNodesChanged(TreeModelEvent e) {
|
public void treeNodesChanged(TreeModelEvent e) {
|
||||||
|
|
||||||
// This is used to register link-file nodes which may be added to the tree as a result
|
Object[] changedChildren = e.getChildren();
|
||||||
// of changes to the associated project data.
|
if (changedChildren != null) {
|
||||||
|
for (Object child : changedChildren) {
|
||||||
Object treeNode = e.getTreePath().getLastPathComponent();
|
treeNodeChanged(child, false);
|
||||||
if (treeNode instanceof DomainFileNode fileNode) {
|
}
|
||||||
addLinkFile(fileNode);
|
}
|
||||||
|
else {
|
||||||
|
treeNodeChanged(e.getTreePath().getLastPathComponent(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,6 +479,19 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
// Link tracking tree update support
|
// Link tracking tree update support
|
||||||
//
|
//
|
||||||
|
|
||||||
|
private void addLoadedChildren(DataTreeNode node) {
|
||||||
|
|
||||||
|
if (!node.isLoaded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GTreeNode child : node.getChildren()) {
|
||||||
|
if (child instanceof DomainFileNode fileNode) {
|
||||||
|
addLinkFile(fileNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update link tree if the specified {@code domainFileNode} corresponds to an link-file
|
* Update link tree if the specified {@code domainFileNode} corresponds to an link-file
|
||||||
* which has an internal link-path which links to either a file or folder within the same
|
* which has an internal link-path which links to either a file or folder within the same
|
||||||
|
@ -403,6 +510,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
String linkPath = LinkHandler.getAbsoluteLinkPath(file);
|
String linkPath = LinkHandler.getAbsoluteLinkPath(file);
|
||||||
if (linkPath == null) {
|
if (linkPath == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -420,6 +528,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
if (isFolderLink) {
|
if (isFolderLink) {
|
||||||
folderLinkNode.addLinkedFolder(domainFileNode);
|
folderLinkNode.addLinkedFolder(domainFileNode);
|
||||||
|
addLoadedChildren(domainFileNode);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode);
|
folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode);
|
||||||
|
@ -439,20 +548,35 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
* once if a {@code LinkedTreeNode} is found which corresponds to the specified
|
* once if a {@code LinkedTreeNode} is found which corresponds to the specified
|
||||||
* {@code parentFolder}. This allows targeted refresh of link-files.
|
* {@code parentFolder}. This allows targeted refresh of link-files.
|
||||||
*
|
*
|
||||||
* @param parentFolder a parent folder which relates to a change
|
* @param parentFolderPath the parent folder path which relates to a change
|
||||||
* @param folderNodeConsumer optional consumer which will be invoked for each loaded parent
|
* @param folderNodeConsumer optional consumer which will be invoked for each loaded parent
|
||||||
* tree node which is a linked-reflection of the specified {@code parentFolder}. If null is
|
* tree node which is a linked-reflection of the specified {@code parentFolder}. If null is
|
||||||
* specified for this consumer a general update will be performed to remove any missing nodes.
|
* specified for this consumer a general update will be performed to remove any missing nodes.
|
||||||
* @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
|
* @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
|
||||||
* is found which corresponds to the specified {@code parentFolder}.
|
* is found which corresponds to the specified {@code parentFolder}.
|
||||||
*/
|
*/
|
||||||
void updateLinkedContent(DomainFolder parentFolder, Consumer<DataTreeNode> folderNodeConsumer,
|
private void updateLinkedContent(String parentFolderPath,
|
||||||
Consumer<LinkedTreeNode> linkNodeConsumer) {
|
Consumer<DataTreeNode> folderNodeConsumer, Consumer<LinkedTreeNode> linkNodeConsumer) {
|
||||||
|
|
||||||
|
if (!Swing.isSwingThread()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Listener and all node updates must operate in Swing thread");
|
||||||
|
}
|
||||||
|
|
||||||
if (skipLinkUpdate) {
|
if (skipLinkUpdate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String pathname = parentFolder.getPathname();
|
|
||||||
String[] pathElements = pathname.split("/");
|
// NOTE: This method must track those paths which have been refreshed to avoid the
|
||||||
|
// possibility of infinite recursion when circular links exist.
|
||||||
|
boolean clearRefreshedTrackingSet = false;
|
||||||
|
if (refreshedTrackingSet == null) {
|
||||||
|
refreshedTrackingSet = new HashSet<>();
|
||||||
|
clearRefreshedTrackingSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] pathElements = parentFolderPath.split("/");
|
||||||
LinkedTreeNode folderLinkNode = linkTreeRoot;
|
LinkedTreeNode folderLinkNode = linkTreeRoot;
|
||||||
folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
|
folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
|
||||||
for (int i = 1; i < pathElements.length; i++) {
|
for (int i = 1; i < pathElements.length; i++) {
|
||||||
|
@ -469,9 +593,18 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
linkNodeConsumer.accept(folderLinkNode);
|
linkNodeConsumer.accept(folderLinkNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
if (clearRefreshedTrackingSet) {
|
||||||
|
refreshedTrackingSet = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class LinkedTreeNode {
|
private class LinkedTreeNode {
|
||||||
|
|
||||||
|
// NOTE: The use of HashSet to track LinkedTreeNodes relies on identity hashcode and
|
||||||
|
// same instance for equality.
|
||||||
|
|
||||||
private final LinkedTreeNode parent;
|
private final LinkedTreeNode parent;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
|
@ -491,7 +624,14 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
|
|
||||||
boolean updateThisNode = subFolderPathIndex >= pathElements.length;
|
boolean updateThisNode = subFolderPathIndex >= pathElements.length;
|
||||||
|
|
||||||
for (DomainFileNode folderLink : folderLinks) {
|
Iterator<DomainFileNode> folderLinkIter = folderLinks.iterator();
|
||||||
|
while (folderLinkIter.hasNext()) {
|
||||||
|
|
||||||
|
DomainFileNode folderLink = folderLinkIter.next();
|
||||||
|
if (folderLink.getParent() == null) {
|
||||||
|
// Remove disposed link node
|
||||||
|
folderLinkIter.remove();
|
||||||
|
}
|
||||||
|
|
||||||
if (!folderLink.isLoaded()) {
|
if (!folderLink.isLoaded()) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -529,6 +669,25 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshLinks(String childName) {
|
private void refreshLinks(String childName) {
|
||||||
|
|
||||||
|
String childPathName = LocalFileSystem.getPath(getPathname(), childName);
|
||||||
|
if (!refreshedTrackingSet.add(childPathName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If links are defined be sure to visit DomainFolder so that we pickup on change
|
||||||
|
// events even if not visible within tree.
|
||||||
|
// TODO: Should no longer be needed after changes were made to force domain folder events
|
||||||
|
// which would affect discovered link-files
|
||||||
|
// if (!folderMap.isEmpty()) {
|
||||||
|
// String path = LocalFileSystem.getPath(getPathname(), childName);
|
||||||
|
// DomainFolder folder =
|
||||||
|
// projectData.getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
|
||||||
|
// if (folder != null) {
|
||||||
|
// folder.getFolders(); // forced visit to folder
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// We are forced to refresh file-links and folder-links since a folder-link may be
|
// We are forced to refresh file-links and folder-links since a folder-link may be
|
||||||
// referencing another folder-link file and not the final referenced folder.
|
// referencing another folder-link file and not the final referenced folder.
|
||||||
if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
|
if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
|
||||||
|
@ -537,10 +696,28 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean refreshFolderLinks(String folderName) {
|
private boolean refreshFolderLinks(String folderName) {
|
||||||
|
|
||||||
LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
|
LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
|
||||||
if (linkedTreeNode != null) {
|
if (linkedTreeNode != null) {
|
||||||
|
|
||||||
refresh(linkedTreeNode.folderLinks);
|
refresh(linkedTreeNode.folderLinks);
|
||||||
return linkedTreeNode.folderLinks.isEmpty();
|
boolean removed = linkedTreeNode.folderLinks.isEmpty();
|
||||||
|
|
||||||
|
// Refresh all file links refering to files within this folder
|
||||||
|
Collection<Set<DomainFileNode>> linkedFileSets =
|
||||||
|
linkedTreeNode.linkedFilesMap.values();
|
||||||
|
if (!linkedFileSets.isEmpty()) {
|
||||||
|
Iterator<Set<DomainFileNode>> iterator = linkedFileSets.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Set<DomainFileNode> linkFileSet = iterator.next();
|
||||||
|
refresh(linkFileSet);
|
||||||
|
if (linkFileSet.isEmpty()) {
|
||||||
|
iterator.remove();
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -557,12 +734,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or get existing named child folder node for this folder node
|
||||||
|
* @param folderName child folder node
|
||||||
|
* @return new or existing named child folder node
|
||||||
|
*/
|
||||||
private LinkedTreeNode addFolder(String folderName) {
|
private LinkedTreeNode addFolder(String folderName) {
|
||||||
return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n));
|
return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLinkedFolder(DomainFileNode folderLink) {
|
/**
|
||||||
folderLinks.add(folderLink);
|
* Add a folder-link which references this folder node
|
||||||
|
* @param folderLink link which references this folder node
|
||||||
|
* @return true if the set did not already contain the specified folderLink
|
||||||
|
*/
|
||||||
|
private boolean addLinkedFolder(DomainFileNode folderLink) {
|
||||||
|
return folderLinks.add(folderLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLinkedFile(String fileName, DomainFileNode fileLink) {
|
private void addLinkedFile(String fileName, DomainFileNode fileLink) {
|
||||||
|
@ -579,23 +766,27 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void refresh(Set<DomainFileNode> linkFiles) {
|
private void refresh(Set<DomainFileNode> linkFiles) {
|
||||||
List<DomainFileNode> purgeList = null;
|
Iterator<DomainFileNode> linkFileIter = linkFiles.iterator();
|
||||||
for (DomainFileNode fileLink : linkFiles) {
|
while (linkFileIter.hasNext()) {
|
||||||
DomainFile file = fileLink.getDomainFile();
|
DomainFileNode fileLink = linkFileIter.next();
|
||||||
// Perform lazy purge of missing link files
|
if (fileLink.getParent() == null || !fileLink.getDomainFile().isLink()) {
|
||||||
if (!file.isLink()) {
|
linkFileIter.remove();
|
||||||
if (purgeList == null) {
|
|
||||||
purgeList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
purgeList.add(fileLink);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
fileLink.refresh();
|
fileLink.refresh();
|
||||||
|
|
||||||
|
GTreeNode linkParent = fileLink.getParent();
|
||||||
|
if (linkParent instanceof DomainFolderNode linkParentNode) {
|
||||||
|
|
||||||
|
// TODO: What about LinkedDomainFolders?
|
||||||
|
ChangeManager.this.updateLinkedContent(linkParentNode.getPathname(), fn -> {
|
||||||
|
/* do nothing */ }, ltn -> {
|
||||||
|
ltn.refreshLinks(fileLink.getName());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if (purgeList != null) {
|
|
||||||
linkFiles.removeAll(purgeList);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,33 @@ public class DataTree extends GTree {
|
||||||
}
|
}
|
||||||
else if (node instanceof DomainFileNode fileNode) {
|
else if (node instanceof DomainFileNode fileNode) {
|
||||||
if (fileNode.isFolderLink()) {
|
if (fileNode.isFolderLink()) {
|
||||||
|
folder = getLinkedFolder(fileNode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Handle normal file cases where we return node's parent folder
|
||||||
|
GTreeNode parent = node.getParent();
|
||||||
|
if (parent instanceof DomainFolderNode folderNode) {
|
||||||
|
folder = folderNode.getDomainFolder();
|
||||||
|
}
|
||||||
|
else if (parent instanceof DomainFileNode parentFileNode) {
|
||||||
|
folder = getLinkedFolder(parentFileNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
||||||
|
// Resolve linked internal folder to its real folder
|
||||||
|
try {
|
||||||
|
folder = linkedFolder.getRealFolder();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
folder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DomainFolder getLinkedFolder(DomainFileNode fileNode) {
|
||||||
// Handle case where file node corresponds to a folder-link.
|
// Handle case where file node corresponds to a folder-link.
|
||||||
// Folder-Link status needs to be checked to ensure it corresponds to a folder
|
// Folder-Link status needs to be checked to ensure it corresponds to a folder
|
||||||
// internal to the same project.
|
// internal to the same project.
|
||||||
|
@ -154,25 +181,6 @@ public class DataTree extends GTree {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Get linked folder - status check ensures null will not be returned
|
// Get linked folder - status check ensures null will not be returned
|
||||||
folder = linkInfo.getLinkedFolder();
|
return linkInfo.getLinkedFolder();
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Handle normal file cases where we return node's parent folder
|
|
||||||
GTreeNode parent = node.getParent();
|
|
||||||
if (parent instanceof DomainFolderNode folderNode) {
|
|
||||||
folder = folderNode.getDomainFolder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
|
||||||
// Resolve linked internal folder to its real folder
|
|
||||||
try {
|
|
||||||
folder = linkedFolder.getRealFolder();
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
folder = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,9 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
|
||||||
private List<GTreeNode> getDomainParentNodes(List<GTreeNode> nodeList) {
|
private List<GTreeNode> getDomainParentNodes(List<GTreeNode> nodeList) {
|
||||||
List<GTreeNode> parentList = new ArrayList<>();
|
List<GTreeNode> parentList = new ArrayList<>();
|
||||||
for (GTreeNode node : nodeList) {
|
for (GTreeNode node : nodeList) {
|
||||||
if (!node.isLeaf()) {
|
if (node instanceof DomainFolderNode) {
|
||||||
|
// We want to ensure we treat link-file node as not being a parent
|
||||||
|
// for this operation.
|
||||||
parentList.add(node);
|
parentList.add(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,11 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
|
||||||
*/
|
*/
|
||||||
public abstract ProjectData getProjectData();
|
public abstract ProjectData getProjectData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns domain folder/file pathname within project
|
||||||
|
*/
|
||||||
|
public abstract String getPathname();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public abstract int compareTo(GTreeNode node);
|
public abstract int compareTo(GTreeNode node);
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,12 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
|
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
|
||||||
private static final String RIGHT_ARROW = "\u2192";
|
private static final String RIGHT_ARROW = "\u2192";
|
||||||
|
|
||||||
|
// NOTE: We must ensure anything used by sort comparator remains fixed
|
||||||
private final DomainFile domainFile;
|
private final DomainFile domainFile;
|
||||||
|
private final boolean isFolderLink;
|
||||||
|
|
||||||
|
private LinkFileInfo linkInfo;
|
||||||
|
private boolean isLeaf;
|
||||||
|
|
||||||
private volatile String displayName; // name displayed in the tree
|
private volatile String displayName; // name displayed in the tree
|
||||||
private volatile Icon icon = UNKNOWN_FILE_ICON;
|
private volatile Icon icon = UNKNOWN_FILE_ICON;
|
||||||
|
@ -54,15 +59,14 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
private volatile String toolTipText;
|
private volatile String toolTipText;
|
||||||
private AtomicInteger refreshCount = new AtomicInteger();
|
private AtomicInteger refreshCount = new AtomicInteger();
|
||||||
|
|
||||||
private boolean isLeaf = true;
|
|
||||||
private LinkFileInfo linkInfo;
|
|
||||||
private DomainFileFilter filter; // relavent when expand folder-link which is a file
|
private DomainFileFilter filter; // relavent when expand folder-link which is a file
|
||||||
|
|
||||||
private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
|
private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
|
||||||
|
|
||||||
DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
|
DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
|
||||||
this.domainFile = domainFile;
|
this.domainFile = domainFile;
|
||||||
this.linkInfo = domainFile.getLinkInfo();
|
linkInfo = domainFile.getLinkInfo();
|
||||||
|
isFolderLink = linkInfo != null && linkInfo.isFolderLink();
|
||||||
this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
|
this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
|
||||||
displayName = domainFile.getName();
|
displayName = domainFile.getName();
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -84,6 +88,11 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
return domainFile;
|
return domainFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPathname() {
|
||||||
|
return domainFile.getPathname();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLeaf() {
|
public boolean isLeaf() {
|
||||||
return isLeaf;
|
return isLeaf;
|
||||||
|
@ -103,15 +112,11 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
* @return true if file is a folder-link
|
* @return true if file is a folder-link
|
||||||
*/
|
*/
|
||||||
public boolean isFolderLink() {
|
public boolean isFolderLink() {
|
||||||
if (linkInfo != null) {
|
return isFolderLink;
|
||||||
return linkInfo.isFolderLink();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get linked folder which corresponds to this folder-link
|
* Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}).
|
||||||
* (see {@link #isFolderLink()}).
|
|
||||||
* @return linked folder or null if this is not a folder-link
|
* @return linked folder or null if this is not a folder-link
|
||||||
*/
|
*/
|
||||||
LinkedDomainFolder getLinkedFolder() {
|
LinkedDomainFolder getLinkedFolder() {
|
||||||
|
@ -214,12 +219,20 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
private void doRefresh() {
|
private void doRefresh() {
|
||||||
|
|
||||||
isLeaf = true;
|
isLeaf = true;
|
||||||
linkInfo = null;
|
LinkFileInfo updatedLinkInfo = domainFile.getLinkInfo();
|
||||||
|
|
||||||
boolean brokenLink = false;
|
boolean brokenLink = false;
|
||||||
List<String> linkErrors = null;
|
List<String> linkErrors = null;
|
||||||
if (domainFile.isLink()) {
|
|
||||||
linkInfo = domainFile.getLinkInfo();
|
if (isFolderLink != (updatedLinkInfo != null && updatedLinkInfo.isFolderLink())) {
|
||||||
|
// Linked-folder node state changed. Since this alters sort order we can't handle this.
|
||||||
|
// Such a DomainFile state change must be handled by the ChangeManager
|
||||||
|
brokenLink = true;
|
||||||
|
linkErrors = List.of("Unsupported folder-link transition");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
linkInfo = updatedLinkInfo;
|
||||||
|
if (linkInfo != null) {
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
LinkStatus linkStatus =
|
LinkStatus linkStatus =
|
||||||
LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
|
LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
|
||||||
|
@ -227,7 +240,7 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
if (brokenLink) {
|
if (brokenLink) {
|
||||||
linkErrors = errors;
|
linkErrors = errors;
|
||||||
}
|
}
|
||||||
else if (isFolderLink()) {
|
else if (isFolderLink) {
|
||||||
if (linkStatus == LinkStatus.INTERNAL) {
|
if (linkStatus == LinkStatus.INTERNAL) {
|
||||||
isLeaf = false;
|
isLeaf = false;
|
||||||
}
|
}
|
||||||
|
@ -237,11 +250,12 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLeaf) {
|
|
||||||
unloadChildren();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We must always unload any children since a leaf has no children and a folder-link
|
||||||
|
// may be transitioning to a state where its children may need to be re-loaded.
|
||||||
|
unloadChildren();
|
||||||
|
|
||||||
displayName = getFormattedDisplayName();
|
displayName = getFormattedDisplayName();
|
||||||
|
|
||||||
toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors));
|
toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors));
|
||||||
|
@ -289,7 +303,25 @@ public class DomainFileNode extends DataTreeNode {
|
||||||
|
|
||||||
private String getFormattedLinkPath() {
|
private String getFormattedLinkPath() {
|
||||||
|
|
||||||
String linkPath = linkInfo != null ? linkInfo.getLinkPath() : null;
|
String linkPath = null;
|
||||||
|
|
||||||
|
// If link-file is a LinkedDomainFile we must always show an absolute link-path since
|
||||||
|
// relative paths are relative to the real file's location and it would be rather confusing
|
||||||
|
// to show as relative
|
||||||
|
if (domainFile instanceof LinkedDomainFile) {
|
||||||
|
try {
|
||||||
|
// will return GhidraURL or absolute internal path
|
||||||
|
linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// attempt to use stored path, although it may fail as well
|
||||||
|
linkPath = linkInfo.getLinkPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (linkInfo != null) {
|
||||||
|
linkPath = linkInfo.getLinkPath();
|
||||||
|
}
|
||||||
|
|
||||||
if (GhidraURL.isGhidraURL(linkPath)) {
|
if (GhidraURL.isGhidraURL(linkPath)) {
|
||||||
try {
|
try {
|
||||||
URL url = new URL(linkPath);
|
URL url = new URL(linkPath);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.util.List;
|
||||||
|
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
|
|
||||||
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
|
@ -48,6 +49,7 @@ public class DomainFolderNode extends DataTreeNode {
|
||||||
private boolean isEditable;
|
private boolean isEditable;
|
||||||
|
|
||||||
DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) {
|
DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) {
|
||||||
|
|
||||||
this.domainFolder = domainFolder;
|
this.domainFolder = domainFolder;
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
|
|
||||||
|
@ -73,6 +75,11 @@ public class DomainFolderNode extends DataTreeNode {
|
||||||
return domainFolder;
|
return domainFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPathname() {
|
||||||
|
return domainFolder.getPathname();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this node has no children.
|
* Returns true if this node has no children.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -112,17 +112,25 @@ public final class LocalTreeNodeHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) {
|
private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) {
|
||||||
|
try {
|
||||||
|
// NOTE: destFolder should be real folder and not linked-folder
|
||||||
// NOTE: We may have issues since checks are not based on canonical paths
|
// NOTE: We may have issues since checks are not based on canonical paths
|
||||||
if (draggedNode instanceof DomainFolderNode folderNode) {
|
if (draggedNode instanceof DomainFolderNode folderNode) {
|
||||||
// This also checks cases where src/dest projects are using the same repository.
|
// This also checks cases where src/dest projects are using the same repository.
|
||||||
// Unfortunately, it will also prevent cases where shared-project folder
|
// Unfortunately, it will also prevent cases where shared-project folder
|
||||||
// does not contain versioned content and could actually be allowed.
|
// does not contain versioned content and could actually be allowed.
|
||||||
DomainFolder folder = folderNode.getDomainFolder();
|
DomainFolder folder = folderNode.getDomainFolder();
|
||||||
|
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
||||||
|
folder = linkedFolder.getRealFolder();
|
||||||
|
}
|
||||||
return !folder.isSameOrAncestor(destFolder);
|
return !folder.isSameOrAncestor(destFolder);
|
||||||
}
|
}
|
||||||
if (draggedNode instanceof DomainFileNode fileNode) {
|
if (draggedNode instanceof DomainFileNode fileNode) {
|
||||||
DomainFolder folder = fileNode.getDomainFile().getParent();
|
|
||||||
DomainFile file = fileNode.getDomainFile();
|
DomainFile file = fileNode.getDomainFile();
|
||||||
|
if (file instanceof LinkedDomainFile linkedFile) {
|
||||||
|
file = linkedFile.getRealFile();
|
||||||
|
}
|
||||||
|
DomainFolder folder = file.getParent();
|
||||||
if (file.isVersioned()) {
|
if (file.isVersioned()) {
|
||||||
// This also checks cases where src/dest projects are using the same repository.
|
// This also checks cases where src/dest projects are using the same repository.
|
||||||
return !folder.isSame(destFolder);
|
return !folder.isSame(destFolder);
|
||||||
|
@ -130,6 +138,10 @@ public final class LocalTreeNodeHandler
|
||||||
DomainFile destFile = destFolder.getFile(file.getName());
|
DomainFile destFile = destFolder.getFile(file.getName());
|
||||||
return destFile == null || !destFile.equals(file);
|
return destFile == null || !destFile.equals(file);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +177,9 @@ public final class LocalTreeNodeHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (file instanceof LinkedDomainFile linkedFile) {
|
||||||
|
file = linkedFile.getRealFile();
|
||||||
|
}
|
||||||
file.moveTo(destFolder);
|
file.moveTo(destFolder);
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
|
@ -186,6 +201,9 @@ public final class LocalTreeNodeHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (sourceFolder instanceof LinkedDomainFolder linkedFolder) {
|
||||||
|
sourceFolder = linkedFolder.getRealFolder();
|
||||||
|
}
|
||||||
sourceFolder.moveTo(destFolder);
|
sourceFolder.moveTo(destFolder);
|
||||||
}
|
}
|
||||||
catch (DuplicateFileException dfe) {
|
catch (DuplicateFileException dfe) {
|
||||||
|
|
|
@ -17,8 +17,7 @@ package ghidra.framework.main.projectdata.actions;
|
||||||
|
|
||||||
import java.awt.Component;
|
import java.awt.Component;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.*;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import docking.widgets.OptionDialog;
|
import docking.widgets.OptionDialog;
|
||||||
import docking.widgets.OptionDialogBuilder;
|
import docking.widgets.OptionDialogBuilder;
|
||||||
|
@ -65,10 +64,21 @@ public class DeleteProjectFilesTask extends Task {
|
||||||
|
|
||||||
initializeMonitor(monitor);
|
initializeMonitor(monitor);
|
||||||
|
|
||||||
deleteFiles(selectedFiles, monitor);
|
Set<DomainFile> resolvedFiles = resolveLinkedFiles(selectedFiles);
|
||||||
|
|
||||||
deleteFolders(selectedFolders, monitor);
|
Set<DomainFolder> resolvedFolders = resolveLinkedFolders(selectedFolders);
|
||||||
|
|
||||||
|
try {
|
||||||
|
deleteFiles(resolvedFiles, monitor);
|
||||||
|
deleteFolders(resolvedFolders, monitor);
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showReport() {
|
||||||
statistics.showReport(parent);
|
statistics.showReport(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,60 +87,105 @@ public class DeleteProjectFilesTask extends Task {
|
||||||
monitor.initialize(statistics.getFileCount());
|
monitor.initialize(statistics.getFileCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteFiles(Set<DomainFile> files, TaskMonitor monitor) {
|
/**
|
||||||
try {
|
* Domain file comparator for use in establishing order of file removal.
|
||||||
|
* All real files (i.e., non-link-files) must be removed before link-files
|
||||||
|
* afterwhich depth-first applies.
|
||||||
|
*/
|
||||||
|
private static final Comparator<DomainFile> FILE_PATH_COMPARATOR =
|
||||||
|
new Comparator<DomainFile>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(DomainFile o1, DomainFile o2) {
|
||||||
|
|
||||||
|
// Ensure that non-link-files occur first in sorted list
|
||||||
|
boolean isLink1 = o1.isLink();
|
||||||
|
boolean isLink2 = o2.isLink();
|
||||||
|
if (isLink1) {
|
||||||
|
if (!isLink2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isLink2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare paths to ensure deeper paths occur first in sorted list
|
||||||
|
String path1 = o1.getPathname();
|
||||||
|
String path2 = o2.getPathname();
|
||||||
|
return path2.compareTo(path1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Set<DomainFile> resolveLinkedFiles(Set<DomainFile> files) {
|
||||||
|
Set<DomainFile> resolvedFiles = new HashSet<>();
|
||||||
for (DomainFile file : files) {
|
for (DomainFile file : files) {
|
||||||
monitor.checkCancelled();
|
// If file is contained within a linked-folder (LinkedDomainFile) we need to
|
||||||
deleteFile(file);
|
// use the actual linked file. Since we should be dealing with internally
|
||||||
monitor.incrementProgress(1);
|
// linked content IOExceptions are unexpected.
|
||||||
}
|
if (file instanceof LinkedDomainFile linkedFile) {
|
||||||
}
|
|
||||||
catch (CancelledException e) {
|
|
||||||
// just return so that statistics for what completed can be displayed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteFolders(Set<DomainFolder> folders, TaskMonitor monitor) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
file = linkedFile.getRealFile();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
continue; // Skip file if unable to resolve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolvedFiles.add(file);
|
||||||
|
}
|
||||||
|
return resolvedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<DomainFolder> resolveLinkedFolders(Set<DomainFolder> folders) {
|
||||||
|
Set<DomainFolder> resolvedFolders = new HashSet<>();
|
||||||
for (DomainFolder folder : folders) {
|
for (DomainFolder folder : folders) {
|
||||||
|
// If folder is a linked-folder (LinkedDomainFolder) we need to
|
||||||
|
// use the actual linked folder. Since we should be dealing with internally
|
||||||
|
// linked content IOExceptions are unexpected.
|
||||||
|
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
||||||
|
try {
|
||||||
|
folder = linkedFolder.getRealFolder();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
continue; // skip file if unable to resolve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolvedFolders.add(folder);
|
||||||
|
}
|
||||||
|
return resolvedFolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteFiles(Set<DomainFile> files, TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
ArrayList<DomainFile> sortedFiles = new ArrayList<>(files);
|
||||||
|
Collections.sort(sortedFiles, FILE_PATH_COMPARATOR);
|
||||||
|
|
||||||
|
for (DomainFile file : sortedFiles) {
|
||||||
|
monitor.checkCancelled();
|
||||||
|
deleteFile(file, monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteFolders(Set<DomainFolder> folders, TaskMonitor monitor)
|
||||||
|
throws CancelledException {
|
||||||
|
|
||||||
|
for (DomainFolder folder : folders) {
|
||||||
|
monitor.checkCancelled();
|
||||||
deleteFolder(folder, monitor);
|
deleteFolder(folder, monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (CancelledException e) {
|
|
||||||
// just return so that statistics for what completed can be displayed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException {
|
private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
while (folder instanceof LinkedDomainFolder linkedFolder) {
|
|
||||||
if (linkedFolder.isLinked()) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Linked-folder's originating file-link should have been removed instead: " +
|
|
||||||
linkedFolder.getPathname());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
folder = linkedFolder.getRealFolder();
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
Msg.error(this, "Error following linked-folder: " + e.getMessage() + "\n" +
|
|
||||||
folder.getPathname());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DomainFolder subFolder : folder.getFolders()) {
|
for (DomainFolder subFolder : folder.getFolders()) {
|
||||||
monitor.checkCancelled();
|
monitor.checkCancelled();
|
||||||
if (!selectedFolders.contains(subFolder)) {
|
|
||||||
deleteFolder(subFolder, monitor);
|
deleteFolder(subFolder, monitor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (DomainFile file : folder.getFiles()) {
|
for (DomainFile file : folder.getFiles()) {
|
||||||
monitor.checkCancelled();
|
monitor.checkCancelled();
|
||||||
if (!selectedFiles.contains(file)) {
|
if (!selectedFiles.contains(file)) {
|
||||||
deleteFile(file);
|
deleteFile(file, monitor);
|
||||||
monitor.incrementProgress(1);
|
monitor.incrementProgress(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +204,13 @@ public class DeleteProjectFilesTask extends Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteFile(DomainFile file) throws CancelledException {
|
private void deleteFile(DomainFile file, TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (file.isOpen()) {
|
if (file.isOpen()) {
|
||||||
statistics.incrementFileInUse();
|
statistics.incrementFileInUse();
|
||||||
showFileInUseDialog(file);
|
showFileInUseDialog(file);
|
||||||
|
@ -184,7 +245,6 @@ public class DeleteProjectFilesTask extends Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
file.delete();
|
file.delete();
|
||||||
statistics.incrementDeleted();
|
statistics.incrementDeleted();
|
||||||
}
|
}
|
||||||
|
@ -198,6 +258,9 @@ public class DeleteProjectFilesTask extends Task {
|
||||||
throw new CancelledException();
|
throw new CancelledException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
monitor.increment();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int showConfirmDeleteVersionedDialog(DomainFile file) {
|
private int showConfirmDeleteVersionedDialog(DomainFile file) {
|
||||||
|
|
|
@ -31,6 +31,8 @@ class FileCountStatistics {
|
||||||
private int deleted;
|
private int deleted;
|
||||||
|
|
||||||
FileCountStatistics(int fileCount) {
|
FileCountStatistics(int fileCount) {
|
||||||
|
// NOTE: Do the possibility of file duplication through the selection of linked-folder
|
||||||
|
// content, this count is an estimate only
|
||||||
this.fileCount = fileCount;
|
this.fileCount = fileCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,9 +74,10 @@ class FileCountStatistics {
|
||||||
|
|
||||||
public void showReport(Component parent) {
|
public void showReport(Component parent) {
|
||||||
// don't show results if only one file processed.
|
// don't show results if only one file processed.
|
||||||
if (getTotalProcessed() == 1) {
|
if (fileCount == 1 && getTotalProcessed() == 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't show results if all selected files deleted
|
// don't show results if all selected files deleted
|
||||||
if (deleted == fileCount) {
|
if (deleted == fileCount) {
|
||||||
return;
|
return;
|
||||||
|
@ -97,20 +100,24 @@ class FileCountStatistics {
|
||||||
builder.append("<tr><td>In Use: </td><td>").append(fileInUse).append("</td></tr>");
|
builder.append("<tr><td>In Use: </td><td>").append(fileInUse).append("</td></tr>");
|
||||||
}
|
}
|
||||||
if (versionedDeclined > 0) {
|
if (versionedDeclined > 0) {
|
||||||
builder.append("<tr><td> Versioned: </td><td>").append(versionedDeclined).append(
|
builder.append("<tr><td> Versioned: </td><td>")
|
||||||
"</td></tr>");
|
.append(versionedDeclined)
|
||||||
|
.append("</td></tr>");
|
||||||
}
|
}
|
||||||
if (checkedOutVersioned > 0) {
|
if (checkedOutVersioned > 0) {
|
||||||
builder.append("<tr><td>Checked-out: </td><td>").append(checkedOutVersioned).append(
|
builder.append("<tr><td>Checked-out: </td><td>")
|
||||||
"</td></tr>");
|
.append(checkedOutVersioned)
|
||||||
|
.append("</td></tr>");
|
||||||
}
|
}
|
||||||
if (readOnlySkipped > 0) {
|
if (readOnlySkipped > 0) {
|
||||||
builder.append("<tr><td>Read only: </td><td>").append(readOnlySkipped).append(
|
builder.append("<tr><td>Read only: </td><td>")
|
||||||
"</td></tr>");
|
.append(readOnlySkipped)
|
||||||
|
.append("</td></tr>");
|
||||||
}
|
}
|
||||||
if (generalFailure > 0) {
|
if (generalFailure > 0) {
|
||||||
builder.append("<tr><td>Other: </td><td>").append(generalFailure).append(
|
builder.append("<tr><td>Other: </td><td>")
|
||||||
"</td></tr>");
|
.append(generalFailure)
|
||||||
|
.append("</td></tr>");
|
||||||
}
|
}
|
||||||
builder.append("</table>");
|
builder.append("</table>");
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,13 +58,17 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
||||||
|
|
||||||
// Confirm the delete *without* using a task so that do not have 2 dialogs showing
|
// Confirm the delete *without* using a task so that do not have 2 dialogs showing
|
||||||
int fileCount = countTask.getFileCount();
|
int fileCount = countTask.getFileCount();
|
||||||
if (!confirmDelete(fileCount, files, context.getComponent())) {
|
if (!confirmDelete(fileCount, files, folders, context.getComponent())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task 2 - perform the delete--this could take a while
|
// Task 2 - perform the delete--this could take a while
|
||||||
DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount);
|
DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount);
|
||||||
TaskLauncher.launch(deleteTask);
|
TaskLauncher.launch(deleteTask);
|
||||||
|
|
||||||
|
if (!deleteTask.isCancelled()) {
|
||||||
|
deleteTask.showReport();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set<DomainFile> files,
|
DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set<DomainFile> files,
|
||||||
|
@ -72,9 +76,10 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
||||||
return new DeleteProjectFilesTask(folders, files, fileCount, context.getComponent());
|
return new DeleteProjectFilesTask(folders, files, fileCount, context.getComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean confirmDelete(int fileCount, Set<DomainFile> files, Component parent) {
|
private boolean confirmDelete(int fileCount, Set<DomainFile> files, Set<DomainFolder> folders,
|
||||||
|
Component parent) {
|
||||||
|
|
||||||
String message = getMessage(fileCount, files);
|
String message = getMessage(fileCount, files, folders);
|
||||||
OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message);
|
OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message);
|
||||||
int choice = builder.addOption("OK")
|
int choice = builder.addOption("OK")
|
||||||
.addCancel()
|
.addCancel()
|
||||||
|
@ -83,28 +88,25 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
|
||||||
return choice != OptionDialog.CANCEL_OPTION;
|
return choice != OptionDialog.CANCEL_OPTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getMessage(int fileCount, Set<DomainFile> selectedFiles) {
|
private String getMessage(int fileCount, Set<DomainFile> files, Set<DomainFolder> folders) {
|
||||||
|
|
||||||
if (fileCount == 0) {
|
if (fileCount == 0) {
|
||||||
return "Are you sure you want to delete the selected empty folder(s)?";
|
return "Are you sure you want to delete the selected empty folder(s)?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (folders.isEmpty()) {
|
||||||
if (fileCount == 1) {
|
if (fileCount == 1) {
|
||||||
if (!selectedFiles.isEmpty()) {
|
DomainFile file = CollectionUtils.any(files);
|
||||||
DomainFile file = CollectionUtils.any(selectedFiles);
|
String type = file.getContentType();
|
||||||
String type = file.isLink() ? "link" : "file";
|
|
||||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete " + type +
|
return "<html>Are you sure you want to <B><U>permanently</U></B> delete " + type +
|
||||||
" \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?";
|
" \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?";
|
||||||
}
|
}
|
||||||
|
|
||||||
// only folders are selected, but they contain files
|
|
||||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " +
|
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " +
|
||||||
" selected files and folders?";
|
" selected files?";
|
||||||
}
|
}
|
||||||
|
|
||||||
// multiple files selected
|
// multiple files selected
|
||||||
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the " + fileCount +
|
return "<html>Are you sure you want to <B><U>permanently</U></B> delete the selected folder(s) and file(s)?";
|
||||||
" selected files?";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -25,8 +25,7 @@ import docking.widgets.tree.GTreeNode;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.framework.main.datatable.ProjectTreeContext;
|
import ghidra.framework.main.datatable.ProjectTreeContext;
|
||||||
import ghidra.framework.main.datatree.*;
|
import ghidra.framework.main.datatree.*;
|
||||||
import ghidra.framework.model.DomainFile;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.model.DomainFolder;
|
|
||||||
import ghidra.util.InvalidNameException;
|
import ghidra.util.InvalidNameException;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
|
|
||||||
|
@ -48,26 +47,32 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isEnabledForContext(T context) {
|
protected boolean isEnabledForContext(T context) {
|
||||||
|
try {
|
||||||
return getFolder(context).isInWritableProject();
|
return getFolder(context).isInWritableProject();
|
||||||
}
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void createNewFolder(T context) {
|
private void createNewFolder(T context) {
|
||||||
|
DomainFolder newFolder = createNewFolderWithDefaultName(context);
|
||||||
DomainFolder parentFolder = getFolder(context);
|
|
||||||
DomainFolder newFolder = createNewFolderWithDefaultName(parentFolder);
|
|
||||||
GTreeNode parent = getParentNode(context);
|
GTreeNode parent = getParentNode(context);
|
||||||
DataTree tree = context.getTree();
|
DataTree tree = context.getTree();
|
||||||
tree.setEditable(true);
|
tree.setEditable(true);
|
||||||
tree.startEditing(parent, newFolder.getName());
|
tree.startEditing(parent, newFolder.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private DomainFolder createNewFolderWithDefaultName(DomainFolder parentFolder) {
|
private DomainFolder createNewFolderWithDefaultName(T context) {
|
||||||
String name = getNewFolderName(parentFolder);
|
String errName = "";
|
||||||
try {
|
try {
|
||||||
|
DomainFolder parentFolder = getFolder(context);
|
||||||
|
String name = getNewFolderName(parentFolder);
|
||||||
|
errName = ": " + name;
|
||||||
return parentFolder.createFolder(name);
|
return parentFolder.createFolder(name);
|
||||||
}
|
}
|
||||||
catch (InvalidNameException | IOException e) {
|
catch (InvalidNameException | IOException e) {
|
||||||
throw new AssertException("Unexpected Error creating new folder: " + name, e);
|
throw new AssertException("Unexpected Error creating new folder" + errName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,18 +87,33 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DomainFolder getFolder(T context) {
|
private DomainFolder getFolder(T context) throws IOException {
|
||||||
// the following code relies on the isAddToPopup to ensure that there is exactly one
|
// the following code relied upon by the isAddToPopup to ensure that there is exactly one
|
||||||
// file or folder selected
|
// file or folder selected
|
||||||
|
DomainFolder folder = null;
|
||||||
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
|
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
|
||||||
return context.getSelectedFolders().get(0);
|
folder = context.getSelectedFolders().get(0);
|
||||||
}
|
}
|
||||||
if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
|
else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
|
||||||
DomainFile file = context.getSelectedFiles().get(0);
|
DomainFile file = context.getSelectedFiles().get(0);
|
||||||
return file.getParent();
|
LinkFileInfo linkInfo = file.getLinkInfo();
|
||||||
|
if (linkInfo != null && linkInfo.isFolderLink()) {
|
||||||
|
folder = linkInfo.getLinkedFolder();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
folder = file.getParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (folder instanceof LinkedDomainFolder linkedFolder) {
|
||||||
|
// Use real folder associated with linked file/folder selection
|
||||||
|
folder = linkedFolder.getRealFolder();
|
||||||
|
}
|
||||||
|
if (folder == null) {
|
||||||
|
// Use root folder if valid selection not found
|
||||||
DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
|
DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
|
||||||
return rootNode.getDomainFolder();
|
folder = rootNode.getDomainFolder();
|
||||||
|
}
|
||||||
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GTreeNode getParentNode(T context) {
|
private GTreeNode getParentNode(T context) {
|
||||||
|
@ -104,9 +124,11 @@ public class ProjectDataNewFolderAction<T extends ProjectTreeContext>
|
||||||
return context.getTree().getModelRoot();
|
return context.getTree().getModelRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node instanceof DomainFileNode) {
|
if (node instanceof DomainFileNode fileNode) {
|
||||||
|
if (!fileNode.isFolderLink()) {
|
||||||
return node.getParent();
|
return node.getParent();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,11 @@ public interface DomainFolderChangeListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify listeners when a domain folder is renamed.
|
* Notify listeners when a domain folder is renamed.
|
||||||
|
* <p>
|
||||||
|
* NOTE: Only a single event will be sent for the specific folder renamed and not its children.
|
||||||
|
* If the listener cares about the impact of this event on the folder's children it will need
|
||||||
|
* to process accordingly.
|
||||||
|
*
|
||||||
* @param folder folder that was renamed
|
* @param folder folder that was renamed
|
||||||
* @param oldName old name of folder
|
* @param oldName old name of folder
|
||||||
*/
|
*/
|
||||||
|
@ -77,6 +82,11 @@ public interface DomainFolderChangeListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification that the domain folder was moved.
|
* Notification that the domain folder was moved.
|
||||||
|
* <p>
|
||||||
|
* NOTE: Only a single event will be sent for the specific folder moved and not its children.
|
||||||
|
* If the listener cares about the impact of this event on the folder's children it will need
|
||||||
|
* to process accordingly.
|
||||||
|
*
|
||||||
* @param folder the folder (after move)
|
* @param folder the folder (after move)
|
||||||
* @param oldParent original parent folder
|
* @param oldParent original parent folder
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -59,8 +59,11 @@ public interface LinkFileInfo {
|
||||||
* method on an {@link #isExternalLink() external-link} will cause the associated
|
* method on an {@link #isExternalLink() external-link} will cause the associated
|
||||||
* project or repository to be opened and associated with the active project as a
|
* project or repository to be opened and associated with the active project as a
|
||||||
* a viewed-project. The resulting folder instance will return true to the method
|
* a viewed-project. The resulting folder instance will return true to the method
|
||||||
* {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links
|
* {@link DomainFolder#isLinked()}.
|
||||||
* which may be chained together.
|
*
|
||||||
|
* NOTE: This method will recurse all internal folder-links which may be chained together
|
||||||
|
* and relies on link status checks to prevent possible recursion (See
|
||||||
|
* {@link LinkHandler#getLinkFileStatus(DomainFile, Consumer)}).
|
||||||
*
|
*
|
||||||
* @return a linked domain folder or null if not a valid folder-link.
|
* @return a linked domain folder or null if not a valid folder-link.
|
||||||
*/
|
*/
|
||||||
|
@ -71,7 +74,10 @@ public interface LinkFileInfo {
|
||||||
* link-file's project or a Ghidra URL.
|
* link-file's project or a Ghidra URL.
|
||||||
* <P>
|
* <P>
|
||||||
* If you want to ensure that a project path returned is absolute and normalized, then
|
* If you want to ensure that a project path returned is absolute and normalized, then
|
||||||
* {@link #getAbsoluteLinkPath()} may be used.
|
* {@link #getAbsoluteLinkPath()} may be used. If this corresponds to a link-file that
|
||||||
|
* implements {@link LinkedDomainFile} the absolute path form must be used to avoid treating
|
||||||
|
* as relative to the incorrect parent folder. A {@link LinkedDomainFile} can occur when
|
||||||
|
* the link-file exists within a linked-folder or subfolder.
|
||||||
*
|
*
|
||||||
* @return associated link path
|
* @return associated link path
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -35,6 +35,6 @@ public interface LinkedDomainFile extends DomainFile {
|
||||||
* @return domain file
|
* @return domain file
|
||||||
* @throws IOException if IO error occurs or file not found
|
* @throws IOException if IO error occurs or file not found
|
||||||
*/
|
*/
|
||||||
public DomainFile getLinkedFile() throws IOException;
|
public DomainFile getRealFile() throws IOException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package ghidra.framework.data;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import generic.test.AbstractGenericTest;
|
||||||
|
|
||||||
|
public class RelativePathTest extends AbstractGenericTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetRelativePath() {
|
||||||
|
|
||||||
|
// File links
|
||||||
|
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", false));
|
||||||
|
assertEquals("../b", GhidraFolderData.getRelativePath("/a/b", "/a/b", false));
|
||||||
|
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", false));
|
||||||
|
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", false));
|
||||||
|
|
||||||
|
// Folder links
|
||||||
|
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", true));
|
||||||
|
assertEquals(".", GhidraFolderData.getRelativePath("/a/b", "/a/b", true));
|
||||||
|
assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", true));
|
||||||
|
assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", true));
|
||||||
|
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b/", "/a/b", true)); // See Note-1
|
||||||
|
assertEquals(".", GhidraFolderData.getRelativePath("/a/b/", "/a/b", true));
|
||||||
|
assertEquals("c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b", true));
|
||||||
|
assertEquals("../c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b/d", true));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -57,6 +57,8 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
// NOTE: TestDummyDomainFolder is inadequate to test against linked-folder cases
|
||||||
|
// which requires real project data support.
|
||||||
root = new TestDummyDomainFolder(null, "root");
|
root = new TestDummyDomainFolder(null, "root");
|
||||||
a = root.createFolder("a");
|
a = root.createFolder("a");
|
||||||
b = root.createFolder("b");
|
b = root.createFolder("b");
|
||||||
|
@ -488,9 +490,9 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
|
||||||
|
|
||||||
private void runAction() {
|
private void runAction() {
|
||||||
|
|
||||||
ActionContext context = new ProjectDataContext(/*provider*/null, /*project data*/null,
|
ActionContext context =
|
||||||
/*context object*/ null, CollectionUtils.asList(folders), CollectionUtils.asList(files),
|
new ProjectDataContext(/*provider*/null, /*project data*/null, /*context object*/ null,
|
||||||
null, true);
|
CollectionUtils.asList(folders), CollectionUtils.asList(files), null, true);
|
||||||
performAction(deleteAction, context, false);
|
performAction(deleteAction, context, false);
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
}
|
}
|
||||||
|
|
|
@ -526,7 +526,6 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
|
||||||
// of folder or another folder-link-file at the referenced location
|
// of folder or another folder-link-file at the referenced location
|
||||||
//
|
//
|
||||||
String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
|
String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
|
||||||
urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/'
|
|
||||||
|
|
||||||
assertEquals(urlPath, linkInfo.getLinkPath());
|
assertEquals(urlPath, linkInfo.getLinkPath());
|
||||||
|
|
||||||
|
@ -593,7 +592,7 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
|
||||||
viewTreeHelper.getDomainFileActionContext(f1LinkFile);
|
viewTreeHelper.getDomainFileActionContext(f1LinkFile);
|
||||||
|
|
||||||
URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
|
URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
|
||||||
"Test", "/f1Link", null);
|
"Test", "/f1Link/", null);
|
||||||
|
|
||||||
DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
|
DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
|
||||||
assertNotNull("Copy action not found", copyAction);
|
assertNotNull("Copy action not found", copyAction);
|
||||||
|
|
|
@ -0,0 +1,420 @@
|
||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package ghidra.framework.main.datatree;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import org.junit.*;
|
||||||
|
|
||||||
|
import docking.widgets.tree.GTree;
|
||||||
|
import docking.widgets.tree.GTreeNode;
|
||||||
|
import ghidra.framework.data.FolderLinkContentHandler;
|
||||||
|
import ghidra.framework.model.DomainFile;
|
||||||
|
import ghidra.framework.model.DomainFolder;
|
||||||
|
import ghidra.program.database.ProgramLinkContentHandler;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.test.*;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
public class ProjectDataTreeTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
|
||||||
|
private FrontEndTestEnv env;
|
||||||
|
|
||||||
|
private DomainFile programAFile;
|
||||||
|
|
||||||
|
private Program program;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
env = new FrontEndTestEnv();
|
||||||
|
program = ToyProgramBuilder.buildSimpleProgram("foo", this);
|
||||||
|
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
programAFile = rootFolder.getFile("Program_A");
|
||||||
|
assertNotNull(programAFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
if (program != null) {
|
||||||
|
program.release(this);
|
||||||
|
}
|
||||||
|
env.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinkFileUpdate() throws Exception {
|
||||||
|
|
||||||
|
GTree tree = env.getTree();
|
||||||
|
GTreeNode modelRoot = tree.getModelRoot();
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
|
||||||
|
DomainFolder aFolder = rootFolder.createFolder("A");
|
||||||
|
|
||||||
|
// file link created before referenced file
|
||||||
|
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
|
||||||
|
ProgramLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("B")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
// Add file 'x' while folder A and linked-folder B are both expanded
|
||||||
|
aFolder.createFile("x", program, TaskMonitor.DUMMY);
|
||||||
|
program.release(this);
|
||||||
|
program = null;
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good state after everything created
|
||||||
|
//
|
||||||
|
// /A
|
||||||
|
// x
|
||||||
|
// y -> x
|
||||||
|
// /B -> /A (linked-folder)
|
||||||
|
// x
|
||||||
|
// y -> x
|
||||||
|
//
|
||||||
|
|
||||||
|
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
|
||||||
|
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
String tip = yNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
|
||||||
|
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
// Remove 'x' file and verify broken links are reflected
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
xNode.getDomainFile().delete();
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
assertNull(aFolderNode.getChild("x"));
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) aFolderNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNull(xNode);
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinkFileUpdate1() throws Exception {
|
||||||
|
|
||||||
|
GTree tree = env.getTree();
|
||||||
|
GTreeNode modelRoot = tree.getModelRoot();
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
|
||||||
|
DomainFolder aFolder = rootFolder.createFolder("A");
|
||||||
|
|
||||||
|
// file link created before referenced file
|
||||||
|
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
|
||||||
|
ProgramLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
// Add file 'x' before folder A and is expanded and linked-folder B is not
|
||||||
|
aFolder.createFile("x", program, TaskMonitor.DUMMY);
|
||||||
|
program.release(this);
|
||||||
|
program = null;
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
|
||||||
|
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good state after everything created
|
||||||
|
//
|
||||||
|
// /A
|
||||||
|
// x
|
||||||
|
// y -> x
|
||||||
|
// /B -> /A (linked-folder)
|
||||||
|
// x
|
||||||
|
// y -> x
|
||||||
|
//
|
||||||
|
|
||||||
|
DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
|
||||||
|
DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
String tip = yNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
// Remove 'x' file and verify broken links are reflected
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
xNode.getDomainFile().delete();
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
assertNull(aFolderNode.getChild("x"));
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) aFolderNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
xNode = (DomainFileNode) aFolderNode.getChild("x");
|
||||||
|
assertNull(xNode);
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinkFileUpdate2() throws Exception {
|
||||||
|
|
||||||
|
GTree tree = env.getTree();
|
||||||
|
GTreeNode modelRoot = tree.getModelRoot();
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
|
||||||
|
DomainFolder aFolder = rootFolder.createFolder("A");
|
||||||
|
|
||||||
|
// file link created before referenced file
|
||||||
|
aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
|
||||||
|
ProgramLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
|
||||||
|
Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
// Add file 'x' while linked-folder B is expanded and folder A is not
|
||||||
|
DomainFile xFile = aFolder.createFile("x", program, TaskMonitor.DUMMY);
|
||||||
|
program.release(this);
|
||||||
|
program = null;
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
//// Verify good state after everything created (leave A collapsed)
|
||||||
|
|
||||||
|
DomainFileNode xNode = (DomainFileNode) bFolderLinkNode.getChild("x");
|
||||||
|
assertNotNull(xNode);
|
||||||
|
|
||||||
|
DomainFileNode yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
String tip = yNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
//// Remove 'x' file
|
||||||
|
|
||||||
|
xFile.delete();
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
assertNull(bFolderLinkNode.getChild("x"));
|
||||||
|
|
||||||
|
yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
|
||||||
|
assertNotNull(yNode);
|
||||||
|
waitForRefresh(yNode);
|
||||||
|
|
||||||
|
tip = yNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinkFileUpdate3() throws Exception {
|
||||||
|
|
||||||
|
GTree tree = env.getTree();
|
||||||
|
GTreeNode modelRoot = tree.getModelRoot();
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
DomainFolder usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
// Add file 'bash'
|
||||||
|
DomainFile bashFile = usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
|
||||||
|
program.release(this);
|
||||||
|
program = null;
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
DomainFileNode binFolderLinkNode = (DomainFileNode) modelRoot.getChild("bin");
|
||||||
|
assertNotNull(binFolderLinkNode.getChild("bash"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// /bin -> /usr/bin (linked folder)
|
||||||
|
// bash
|
||||||
|
// /usr
|
||||||
|
// /bin
|
||||||
|
// bash
|
||||||
|
//
|
||||||
|
|
||||||
|
// Delete real folders and content
|
||||||
|
bashFile.delete();
|
||||||
|
usrBinFolder.delete(); // /usr/bin
|
||||||
|
rootFolder.getFolder("usr").delete();
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
assertNull(binFolderLinkNode.getChild("bash"));
|
||||||
|
|
||||||
|
waitForRefresh(binFolderLinkNode);
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
String tip = binFolderLinkNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
|
||||||
|
// binLinkFile.delete();
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
// Re-create content
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
program = (Program) programAFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
|
||||||
|
assertNotNull(program);
|
||||||
|
usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
|
||||||
|
program.release(this);
|
||||||
|
program = null;
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
DomainFileNode xLinkedFileNode = (DomainFileNode) binFolderLinkNode.getChild("bash");
|
||||||
|
assertNotNull(xLinkedFileNode);
|
||||||
|
|
||||||
|
tip = binFolderLinkNode.getToolTip();
|
||||||
|
assertFalse(tip.contains("Broken"));
|
||||||
|
|
||||||
|
// Repeat removal of folder A and its contents
|
||||||
|
bashFile = usrBinFolder.getFile("bash");
|
||||||
|
assertNotNull(bashFile);
|
||||||
|
bashFile.delete();
|
||||||
|
usrBinFolder.delete();
|
||||||
|
rootFolder.getFolder("usr").delete();
|
||||||
|
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
assertNull(binFolderLinkNode.getChild("bash"));
|
||||||
|
|
||||||
|
waitForRefresh(binFolderLinkNode);
|
||||||
|
env.waitForTree();
|
||||||
|
|
||||||
|
tip = binFolderLinkNode.getToolTip();
|
||||||
|
assertTrue(tip.contains("Broken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForRefresh(DomainFileNode fileNode) {
|
||||||
|
waitFor(new BooleanSupplier() {
|
||||||
|
@Override
|
||||||
|
public boolean getAsBoolean() {
|
||||||
|
return !fileNode.hasPendingRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import ghidra.program.database.ProgramLinkContentHandler;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.server.remote.ServerTestUtil;
|
import ghidra.server.remote.ServerTestUtil;
|
||||||
import ghidra.test.*;
|
import ghidra.test.*;
|
||||||
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.exception.DuplicateFileException;
|
import ghidra.util.exception.DuplicateFileException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
@ -50,13 +51,16 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
/abc/ (folder)
|
/abc/ (folder)
|
||||||
abc -> /xyz/abc (circular)
|
abc -> /xyz/abc (circular folder allowed as internal)
|
||||||
foo (program file)
|
foo (program file)
|
||||||
/xyz/
|
/xyz/
|
||||||
abc -> /abc (folder link)
|
abc -> /abc (folder link)
|
||||||
abc -> (circular)
|
abc -> /xyz/abc (circular folder allowed as internal)
|
||||||
foo
|
foo
|
||||||
foo -> /abc/foo (program link)
|
foo -> /abc/foo (program link)
|
||||||
|
/e -> f (circular folder link path)
|
||||||
|
/f -> g (circular folder link path)
|
||||||
|
/g -> e (circular folder link path)
|
||||||
**/
|
**/
|
||||||
|
|
||||||
DomainFolder rootFolder = env.getRootFolder();
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
@ -72,6 +76,18 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
||||||
|
|
||||||
programFile.copyToAsLink(xyzFolder, false);
|
programFile.copyToAsLink(xyzFolder, false);
|
||||||
|
|
||||||
|
// Circular folder-link path without real folder
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/f", true, "e",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/g", true, "f",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(), "/e", true, "g",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
|
rootFolder.createLinkFile(rootFolder.getProjectData(),
|
||||||
|
"/home/tsharr2/Examples/linktest/usr/lib64/../lib64", true, "nested2lib64",
|
||||||
|
FolderLinkContentHandler.INSTANCE);
|
||||||
|
|
||||||
env.waitForTree();
|
env.waitForTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,23 +273,154 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrokenFolderLink() throws Exception {
|
public void testCircularFolderLink1() throws Exception {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verify broken folder-link status for /abc/abc which has circular reference
|
// Verify broken folder-link status for /e which has circular reference
|
||||||
|
//
|
||||||
|
DomainFileNode eLinkNode = waitForFileNode("/e");
|
||||||
|
assertTrue(eLinkNode.isFolderLink());
|
||||||
|
String displayName = runSwing(() -> eLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(eLinkNode.getDomainFile(), null));
|
||||||
|
String tooltip = eLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("circular"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify broken folder-link status for /g which has circular reference
|
||||||
|
//
|
||||||
|
DomainFileNode gLinkNode = waitForFileNode("/g");
|
||||||
|
assertTrue(gLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> gLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = gLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("circular"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify broken folder-link status for /f which has circular reference
|
||||||
|
//
|
||||||
|
DomainFileNode fLinkNode = waitForFileNode("/f");
|
||||||
|
assertTrue(fLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> fLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = fLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("circular"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Rename folder /e to /ABC causing folder-links to have broken path
|
||||||
|
//
|
||||||
|
Swing.runNow(() -> eLinkNode.setName("ABC"));
|
||||||
|
|
||||||
|
env.waitForTree(); // give time for ChangeManager to update
|
||||||
|
|
||||||
|
// Verify /e node not found
|
||||||
|
assertNull(env.getRootNode().getChild("e"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify broken folder-link status for /ABC (final folder /e not found)
|
||||||
|
//
|
||||||
|
DomainFileNode abcLinkNode = waitForFileNode("/ABC");
|
||||||
|
assertTrue(abcLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> abcLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = abcLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("folder not found: /e"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify broken folder-link status for /g (final folder /e not found)
|
||||||
|
//
|
||||||
|
waitForFileNode("/g");
|
||||||
|
assertTrue(gLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> gLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = gLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("folder not found: /e"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify broken folder-link status for /f (final folder /e not found)
|
||||||
|
//
|
||||||
|
waitForFileNode("/f");
|
||||||
|
assertTrue(fLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> fLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
|
||||||
|
assertEquals(LinkStatus.BROKEN,
|
||||||
|
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = fLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertTrue(tooltip.contains("folder not found: /e"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create folder /e
|
||||||
|
//
|
||||||
|
DomainFolder rootFolder = env.getRootFolder();
|
||||||
|
rootFolder.createFolder("e");
|
||||||
|
|
||||||
|
env.waitForTree(); // give time for ChangeManager to update
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good folder-link status for /ABC
|
||||||
|
//
|
||||||
|
waitForFileNode("/ABC");
|
||||||
|
assertTrue(abcLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> abcLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
|
||||||
|
assertEquals(LinkStatus.INTERNAL,
|
||||||
|
LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = gLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertFalse(tooltip.contains("folder not found"));
|
||||||
|
assertFalse(tooltip.contains("circular"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good folder-link status for /g
|
||||||
|
//
|
||||||
|
waitForFileNode("/g");
|
||||||
|
assertTrue(gLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> gLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
|
||||||
|
assertEquals(LinkStatus.INTERNAL,
|
||||||
|
LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = gLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertFalse(tooltip.contains("folder not found"));
|
||||||
|
assertFalse(tooltip.contains("circular"));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good folder-link status for /f
|
||||||
|
//
|
||||||
|
waitForFileNode("/f");
|
||||||
|
assertTrue(fLinkNode.isFolderLink());
|
||||||
|
displayName = runSwing(() -> fLinkNode.getDisplayText());
|
||||||
|
assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
|
||||||
|
assertEquals(LinkStatus.INTERNAL,
|
||||||
|
LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
|
||||||
|
tooltip = fLinkNode.getToolTip().replace(" ", " ");
|
||||||
|
assertFalse(tooltip.contains("folder not found"));
|
||||||
|
assertFalse(tooltip.contains("circular"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircularFolderLink2() throws Exception {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify good folder-link internal status for /abc/abc which has allowed circular reference
|
||||||
//
|
//
|
||||||
DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
|
DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
|
||||||
assertTrue(abcAbcLinkNode.isFolderLink());
|
assertTrue(abcAbcLinkNode.isFolderLink());
|
||||||
String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
|
String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
|
||||||
assertTrue("Unexpected node display name: " + displayName,
|
assertTrue("Unexpected node display name: " + displayName,
|
||||||
displayName.endsWith(" /xyz/abc"));
|
displayName.endsWith(" /xyz/abc"));
|
||||||
assertEquals(LinkStatus.BROKEN,
|
assertEquals(LinkStatus.INTERNAL,
|
||||||
LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
|
LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
|
||||||
String tooltip = abcAbcLinkNode.getToolTip().replace(" ", " ");
|
|
||||||
assertTrue(tooltip.contains("circular"));
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verify good folder-link internal status for /xyz/abc which has circular reference
|
// Verify good folder-link internal status for /xyz/abc which has allowed circular reference
|
||||||
//
|
//
|
||||||
DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
|
DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
|
||||||
assertTrue(xyzAbcLinkNode.isFolderLink());
|
assertTrue(xyzAbcLinkNode.isFolderLink());
|
||||||
|
@ -283,17 +430,15 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
||||||
LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
|
LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verify broken folder-link status for /xyz/abc/abc which has circular reference
|
// Verify good folder-link internal status for /xyz/abc/abc which has allowed circular reference
|
||||||
//
|
//
|
||||||
DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc");
|
DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc");
|
||||||
assertTrue(abcLinkedNode.isFolderLink());
|
assertTrue(abcLinkedNode.isFolderLink());
|
||||||
displayName = runSwing(() -> abcLinkedNode.getDisplayText());
|
displayName = runSwing(() -> abcLinkedNode.getDisplayText());
|
||||||
assertTrue("Unexpected node display name: " + displayName,
|
assertTrue("Unexpected node display name: " + displayName,
|
||||||
displayName.endsWith(" /xyz/abc"));
|
displayName.endsWith(" /xyz/abc"));
|
||||||
assertEquals(LinkStatus.BROKEN,
|
assertEquals(LinkStatus.INTERNAL,
|
||||||
LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
|
LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
|
||||||
tooltip = abcLinkedNode.getToolTip().replace(" ", " ");
|
|
||||||
assertTrue(tooltip.contains("circular"));
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken
|
// Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken
|
||||||
|
@ -315,7 +460,7 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
|
||||||
displayName.endsWith(" /xyz/abc"));
|
displayName.endsWith(" /xyz/abc"));
|
||||||
assertEquals(LinkStatus.BROKEN,
|
assertEquals(LinkStatus.BROKEN,
|
||||||
LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
|
LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
|
||||||
tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
|
String tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
|
||||||
assertTrue(tooltip.contains("folder not found: /abc"));
|
assertTrue(tooltip.contains("folder not found: /abc"));
|
||||||
|
|
||||||
env.waitForTree(); // give time for ChangeManager to update
|
env.waitForTree(); // give time for ChangeManager to update
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue