Candidate release of source code.

This commit is contained in:
Dan 2019-03-26 13:45:32 -04:00
parent db81e6b3b0
commit 79d8f164f8
12449 changed files with 2800756 additions and 16 deletions

View file

@ -0,0 +1 @@
MODULE FILE LICENSE: lib/ganymed-ssh2-262.jar Christian Plattner

View file

@ -0,0 +1,11 @@
apply plugin: 'eclipse'
eclipse.project.name = 'Framework FileSystem'
dependencies {
compile project(':Generic')
compile project(':DB')
compile project(':Docking')
compile "ch.ethz.ganymed:ganymed-ssh2:262@jar"
}

View file

@ -0,0 +1,10 @@
##VERSION: 2.0
##MODULE IP: Christian Plattner
.classpath||GHIDRA||||END|
.project||GHIDRA||||END|
Module.manifest||GHIDRA||||END|
build.gradle||GHIDRA||||END|
src/main/java/ghidra/framework/client/package.html||GHIDRA||reviewed||END|
src/main/java/ghidra/framework/store/db/package.html||GHIDRA||reviewed||END|
src/main/java/ghidra/framework/store/local/package.html||GHIDRA||reviewed||END|
src/main/java/ghidra/framework/store/package.html||GHIDRA||reviewed||END|

View file

@ -0,0 +1,91 @@
/* ###
* 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.client;
import java.awt.Component;
import java.net.Authenticator;
import javax.security.auth.callback.*;
import ghidra.framework.remote.AnonymousCallback;
import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.security.KeyStorePasswordProvider;
public interface ClientAuthenticator extends KeyStorePasswordProvider {
/**
* Get a standard Java authenticator for HTTP and other standard network connections
* @return authenticator object
*/
public Authenticator getAuthenticator();
/**
* Process Ghidra Server password authentication callbacks.
* @param title password prompt title if GUI is used
* @param serverType type of server (label associated with serverName)
* @param serverName name of server
* @param nameCb provides storage for user login name. A null indicates
* that the default user name will be used, @see ClientUtil#getUserName().
* @param passCb provides storage for user password, @see PasswordCallback#setPassword(char[])
* @param choiceCb specifies choice between NT Domain authentication (index=0) and local password
* file authentication (index=1). Set selected index to specify authenticator to be used,
* @param anonymousCb may be used to request anonymous read-only access to
* the server. A null is specified if anonymous access has not been enabed on the server.
* @param loginError previous login error message or null for first attempt
* @see ChoiceCallback#setSelectedIndex(int).
* A null is specified if no choice is available (password authenticator determined by server configuration).
* @see AnonymousCallback#setAnonymousAccessRequested(boolean)
* @return
*/
public boolean processPasswordCallbacks(String title, String serverType, String serverName,
NameCallback nameCb, PasswordCallback passCb, ChoiceCallback choiceCb,
AnonymousCallback anonymousCb, String loginError);
/**
* Prompt user for reconnect
* @param parent dialog parent component or null if not applicable
* @param message
* @return return true if reconnect should be attempted
*/
public boolean promptForReconnect(Component parent, final String message);
/**
* Get new user password
* @param parent dialog parent component or null if not applicable
* @param serverInfo server host info
* @param username
* @return new password or null if password should not be changed,
* if not null array will be cleared by caller
*/
public char[] getNewPassword(Component parent, String serverInfo, String username);
/**
* @return true if SSH private key is available for authentication
*/
public boolean isSSHKeyAvailable();
/**
* Process Ghidra Server SSH authentication callbacks.
* @param serverName name of server
* @param nameCb provides storage for user login name. A null indicates
* that the default user name will be used, @see ClientUtil#getUserName().
* @param sshCb provides authentication token to be signed with private key, @see SSHAuthenticationCallback#sign(SSHPrivateKey)
* @return
*/
public boolean processSSHSignatureCallbacks(String serverName, NameCallback nameCb,
SSHSignatureCallback sshCb);
}

View file

@ -0,0 +1,479 @@
/* ###
* 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.client;
import java.awt.Component;
import java.io.IOException;
import java.net.Authenticator;
import java.net.UnknownHostException;
import java.rmi.*;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Hashtable;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import ghidra.framework.model.ServerInfo;
import ghidra.framework.remote.*;
import ghidra.framework.remote.security.SSHKeyManager;
import ghidra.net.*;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.TaskLauncher;
/**
* <code>ClientUtil</code> allows a user to connect to a Repository Server and obtain its handle.
*/
public class ClientUtil {
private static ClientAuthenticator clientAuthenticator;
private static Hashtable<ServerInfo, RepositoryServerAdapter> serverHandles = new Hashtable<>();
private ClientUtil() {
}
/**
* Set client authenticator
* @param authenticator
*/
public static synchronized void setClientAuthenticator(ClientAuthenticator authenticator) {
clientAuthenticator = authenticator;
Authenticator.setDefault(authenticator.getAuthenticator());
SSHKeyManager.setProtectedKeyStorePasswordProvider(clientAuthenticator);
ApplicationKeyManagerFactory.setKeyStorePasswordProvider(clientAuthenticator);
}
/**
* Get the currently installed client authenticator. If one has not been
* installed, this will trigger the installation of a default instance.
* @return current client authenticator
*/
public static ClientAuthenticator getClientAuthenticator() {
if (clientAuthenticator == null) {
if (SystemUtilities.isInHeadlessMode()) {
setClientAuthenticator(new HeadlessClientAuthenticator());
}
else {
setClientAuthenticator(new DefaultClientAuthenticator());
}
}
return clientAuthenticator;
}
/**
* Connect to a Repository Server and obtain a handle to it.
* Based upon the server authentication requirements, the user may be
* prompted for a password via a Swing dialog. If a previous connection
* attempt to this server failed, the adapter may be returned in a
* disconnected state.
* @param host server name or address
* @param port server port, 0 indicates that default port should be used.
* @return repository server adapter
* @throws LoginException thrown if server fails to authenticate user or
* general access is denied.
*/
public static RepositoryServerAdapter getRepositoryServer(String host, int port) {
return getRepositoryServer(host, port, false);
}
/**
* Connect to a Repository Server and obtain a handle to it.
* Based upon the server authentication requirements, the user may be
* prompted for a password via a Swing dialog.
* @param host server name or address
* @param port server port, 0 indicates that default port should be used.
* @param forceConnect if true and the server adapter is disconnected, an
* attempt will be made to reconnect.
* @return repository server handle
* @throws LoginException thrown if server fails to authenticate user or
* general access is denied.
*/
public static RepositoryServerAdapter getRepositoryServer(String host, int port,
boolean forceConnect) {
// ensure that default callback is setup if possible
getClientAuthenticator();
host = host.trim().toLowerCase();
try {
host = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
Msg.warn(ClientUtil.class, "Failed to resolve hostname for " + host);
}
if (port <= 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
ServerInfo server = new ServerInfo(host, port);
RepositoryServerAdapter rsa;
synchronized (serverHandles) {
rsa = serverHandles.get(server);
if (rsa == null) {
rsa = new RepositoryServerAdapter(server);
serverHandles.put(server, rsa);
forceConnect = true;
}
if (forceConnect) {
try {
rsa.connect();
}
catch (NotConnectedException e) {
// message already displayed by RepositoryServerAdapter, so don't handle here
}
}
}
return rsa;
}
/**
* Eliminate the specified repository server from the connection cache
* @param host host name or IP address
* @param port port (0: use default port)
* @throws IOException
*/
public static void clearRepositoryAdapter(String host, int port) throws IOException {
host = host.trim().toLowerCase();
String hostAddr = host;
try {
hostAddr = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
throw new IOException("Repository server lookup failed: " + host);
}
if (port == 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
ServerInfo server = new ServerInfo(hostAddr, port);
RepositoryServerAdapter serverAdapter = serverHandles.remove(server);
if (serverAdapter != null) {
serverAdapter.disconnect();
}
}
/**
* Returns default user login name. Actual user name used by repository
* should be obtained from RepositoryServerAdapter.getUser
*/
public static String getUserName() {
return SystemUtilities.getUserName();
}
/**
*
* Displays an error dialog appropriate for the given exception. If the exception is a
* ConnectException or NotConnectedException, a prompt to reconnect to the Ghidra Server
* is displayed.
*
* @param repository may be null if the exception is not a RemoteException
* @param exc exception that occurred
* @param operation operation that was being done when the exception occurred; this string
* is be used in the message for the error dialog if one should be displayed
* @param mustRetry true if the message should state that the user should retry the operation
* because it may not have succeeded (if the exception was because a RemoteException); there
* may be cases where the operation succeeded; as a result of the operation, a bad connection
* to the server was detected (e.g., save a file). Note: this parameter is ignored if the
* exception is not a ConnectException or NotConnectedException.
* @param parent parent of the error dialog
*/
public static void handleException(RepositoryAdapter repository, Exception exc,
String operation, boolean mustRetry, Component parent) {
String title = "Error During " + operation;
if ((exc instanceof ConnectException) || (exc instanceof NotConnectedException)) {
Msg.debug(ClientUtil.class, "Server not connected (" + operation + ")");
promptForReconnect(repository, operation, mustRetry, parent);
}
else if ((exc instanceof ServerException) || (exc instanceof ServerError)) {
Msg.showError(ClientUtil.class, parent, title,
"Exception occurred on the Ghidra Server.", exc.getCause());
}
else if (exc instanceof RemoteException) {
Msg.showError(ClientUtil.class, parent, title,
"Exception occurred communicating with Ghidra Server.", exc.getCause());
}
else {
String excMsg = exc.getMessage();
if (excMsg == null) {
excMsg = exc.toString();
}
if (exc instanceof IOException) {
Msg.showError(ClientUtil.class, parent, title, excMsg);
}
else {
// show the stacktrace for non-IOException
Msg.showError(ClientUtil.class, parent, title, excMsg, exc);
}
}
}
/**
* Displays an error dialog appropriate for the given exception. If the exception is a
* ConnectException or NotConnectedException, a prompt to reconnect to the Ghidra Server
* is displayed. The message states that the operation may have to be retried due to the
* failed connection.
*
* @param repository may be null if the exception is not a RemoteException
* @param exc exception that occurred
* @param operation operation that was being done when the exception occurred; this string
* is be used in the message for the error dialog if one should be displayed
* @param parent parent of the error dialog
*/
public static void handleException(RepositoryAdapter repository, Exception exc,
String operation, Component parent) {
handleException(repository, exc, operation, true, parent);
}
/**
* Prompt the user to reconnect to the Ghidra Server.
* @param repository repository to connect to
* @param parent parent of the dialog
*/
public static void promptForReconnect(RepositoryAdapter repository, Component parent) {
promptForReconnect(repository, null, false, parent);
}
private static void promptForReconnect(final RepositoryAdapter rep, final String operation,
final boolean mustRetry, final Component parent) {
getClientAuthenticator();
if (clientAuthenticator == null) {
return;
}
final StringBuffer sb = new StringBuffer();
if (mustRetry) {
sb.append("The " + operation +
" may have failed due to a lost connection with the Ghidra Server.\n");
sb.append(
"You may have to retry the operation after you have reconnected to the server.");
}
else {
sb.append("The connection to the Ghidra Server has been lost.");
}
sb.append("\n \nWould you like to reconnect?");
if (rep != null && clientAuthenticator.promptForReconnect(parent, sb.toString())) {
try {
rep.connect();
}
catch (NotConnectedException e) {
// ignore
}
catch (IOException e) {
ClientUtil.handleException(rep, e, "Server Reconnect", null);
}
}
}
/**
* Connect to a Ghidra Server and verify compatibility. This method can be used
* to affectively "ping" the Ghidra Server to verify the ability to connect.
* NOTE: Use of this method when PKI authentication is enabled is not supported.
* @param host server hostname
* @param port first Ghidra Server port (0=use default)
* @throws IOException thrown if an IO Error occurs (e.g., server not found).
* @throws RemoteException if server interface is incompatible or another server-side
* error occurs.
*/
public static void checkGhidraServer(String host, int port) throws IOException {
ServerConnectTask.getGhidraServerHandle(new ServerInfo(host, port));
}
/**
* Connect to a Repository Server and obtain a handle to it.
* Based upon the server authentication requirements, the user may be
* prompted for a name/password via a Swing dialog. If null
* is returned, this indicates that the user cancelled the connect
* operation.
* @param server server address and port
* @return repository server handle
* @throws LoginException thrown if server fails to authenticate user or
* general access is denied.
* @throws GeneralSecurityException if server authentication fails due to
* credential access error (e.g., PKI cert failure)
* @throws IOException thrown if an IO Error occurs.
*/
static RemoteRepositoryServerHandle connect(ServerInfo server)
throws LoginException, GeneralSecurityException, IOException {
getClientAuthenticator();
boolean allowLoginRetry = (clientAuthenticator instanceof DefaultClientAuthenticator);
RemoteRepositoryServerHandle hdl = null;
ServerConnectTask connectTask = new ServerConnectTask(server, allowLoginRetry);
if (!SystemUtilities.isInHeadlessMode() && SystemUtilities.isEventDispatchThread()) {
// Must be done in modal dialog to allow possible authentication prompts
// from another thread.
TaskLauncher.launch(connectTask);
}
else {
connectTask.run(null);
}
hdl = connectTask.getRepositoryServerHandle();
if (hdl == null) {
Exception e = connectTask.getException();
if (e == null) {
return null; // cancelled by user
}
if (e instanceof IOException) {
throw (IOException) e;
}
if (e instanceof LoginException) {
throw (LoginException) e;
}
if (e instanceof GeneralSecurityException) {
throw (GeneralSecurityException) e;
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new AssertException(e);
}
return hdl;
}
/**
* Prompt user and change password on server (not initiated by user).
* @param parent dialog parent
* @param handle server handle
* @param serverInfo server information
* @throws IOException
*/
public static void changePassword(Component parent, RepositoryServerHandle handle,
String serverInfo) throws IOException {
getClientAuthenticator();
if (clientAuthenticator == null) {
return;
}
char[] pwd = null;
try {
pwd = clientAuthenticator.getNewPassword(parent, serverInfo, handle.getUser());
if (pwd != null) {
handle.setPassword(
HashUtilities.getSaltedHash(HashUtilities.SHA256_ALGORITHM, pwd));
Msg.showInfo(ClientUtil.class, parent, "Password Changed",
"Password was changed successfully");
}
}
finally {
if (pwd != null) {
// Attempt to remove traces of password in memory
Arrays.fill(pwd, ' ');
}
}
}
static boolean processPasswordCallbacks(Callback[] callbacks, String serverName,
String defaultUserID, String loginError) throws IOException {
getClientAuthenticator();
if (clientAuthenticator == null) {
Msg.error(ClientUtil.class, "Unable to authenticate user without ClientAuthenticator");
return false;
}
NameCallback nameCb = null;
PasswordCallback passCb = null;
ChoiceCallback choiceCb = null;
AnonymousCallback anonymousCb = null;
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
nameCb = (NameCallback) callback;
nameCb.setName(defaultUserID);
}
else if (callback instanceof PasswordCallback) {
passCb = (PasswordCallback) callback;
}
else if (callback instanceof ChoiceCallback) {
choiceCb = (ChoiceCallback) callback;
}
else if (callback instanceof AnonymousCallback) {
anonymousCb = (AnonymousCallback) callback;
}
}
if (passCb == null) {
throw new IOException(
"Unsupported authentication callback: " + callbacks[0].getClass().getName());
}
if (!clientAuthenticator.processPasswordCallbacks("Repository Server Authentication",
"Repository Server", serverName, nameCb, passCb, choiceCb, anonymousCb, loginError)) {
return false;
}
String name = defaultUserID;
if (nameCb != null) {
name = nameCb.getName();
if (name == null) {
name = nameCb.getDefaultName();
}
}
Msg.info(ClientUtil.class,
"Password authenticating to " + serverName + " as user '" + name + "'");
return true;
}
static void processSignatureCallback(String serverName, SignatureCallback sigCb)
throws IOException {
try {
SignedToken signedToken = ApplicationKeyManagerUtils.getSignedToken(
sigCb.getRecognizedAuthorities(), sigCb.getToken());
sigCb.sign(signedToken.certChain, signedToken.signature);
Msg.info(ClientUtil.class, "PKI Authenticating to " + serverName + " as user '" +
signedToken.certChain[0].getSubjectDN() + "'");
}
catch (Exception e) {
String msg = e.getMessage();
if (msg == null) {
msg = e.toString();
}
throw new IOException(msg, e);
}
}
static boolean processSSHSignatureCallback(Callback[] callbacks, String serverName,
String defaultUserID) {
NameCallback nameCb = null;
SSHSignatureCallback sshCb = null;
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
nameCb = (NameCallback) callback;
nameCb.setName(defaultUserID);
}
else if (callback instanceof SSHSignatureCallback) {
sshCb = (SSHSignatureCallback) callback;
}
}
if (sshCb == null || !clientAuthenticator.isSSHKeyAvailable()) {
return false;
}
if (!clientAuthenticator.processSSHSignatureCallbacks(serverName, nameCb, sshCb)) {
return false;
}
Msg.info(ClientUtil.class,
"SSH Authenticating to " + serverName + " as user '" + defaultUserID + "'");
return true;
}
public static boolean isSSHKeyAvailable() {
return clientAuthenticator.isSSHKeyAvailable();
}
}

View file

@ -0,0 +1,210 @@
/* ###
* 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.client;
import java.awt.Component;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import javax.security.auth.callback.*;
import docking.DockingWindowManager;
import docking.widgets.*;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.remote.AnonymousCallback;
import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
public class DefaultClientAuthenticator extends PopupKeyStorePasswordProvider
implements ClientAuthenticator {
private Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
Msg.debug(this, "PasswordAuthentication requested for " + getRequestingURL());
NameCallback nameCb = null;
if (!"NO_NAME".equals(getRequestingScheme())) {
nameCb = new NameCallback("Name: ", ClientUtil.getUserName());
}
String prompt = getRequestingPrompt();
if (prompt == null) {
prompt = "Password:";
}
PasswordCallback passCb = new PasswordCallback(prompt, false);
ServerPasswordPrompt pp = new ServerPasswordPrompt("Connection Authentication",
"Server", getRequestingHost(), nameCb, passCb, null, null, null);
SystemUtilities.runSwingNow(pp);
if (pp.okWasPressed()) {
return new PasswordAuthentication(nameCb != null ? nameCb.getName() : null,
passCb.getPassword());
}
return null;
}
};
@Override
public Authenticator getAuthenticator() {
return authenticator;
}
@Override
public boolean isSSHKeyAvailable() {
return false; // GUI does not currently support SSH authentication
}
@Override
public boolean processSSHSignatureCallbacks(String serverName, NameCallback nameCb,
SSHSignatureCallback sshCb) {
return false;
}
@Override
public boolean processPasswordCallbacks(String title, String serverType, String serverName,
NameCallback nameCb, PasswordCallback passCb, ChoiceCallback choiceCb,
AnonymousCallback anonymousCb, String loginError) {
ServerPasswordPrompt pp = new ServerPasswordPrompt(title, serverType, serverName, nameCb,
passCb, choiceCb, anonymousCb, loginError);
SystemUtilities.runSwingNow(pp);
return pp.okWasPressed();
}
@Override
public boolean promptForReconnect(final Component parent, final String message) {
final boolean[] retVal = new boolean[] { false };
Runnable r = () -> {
String msg = message;
if (msg != null) {
msg = msg + "\n";
}
msg = msg + "Do you want to reconnect to the server now?";
retVal[0] = OptionDialog.showYesNoDialog(parent, "Lost Connection to Server",
message) == OptionDialog.OPTION_ONE;
};
SystemUtilities.runSwingNow(r);
return retVal[0];
}
@Override
public char[] getNewPassword(final Component parent, String serverInfo, String username) {
final PasswordChangeDialog dlg =
new PasswordChangeDialog("Change Password", "Repository Server", serverInfo, username);
Runnable r = () -> DockingWindowManager.showDialog(parent, dlg);
try {
SystemUtilities.runSwingNow(r);
return dlg.getPassword();
}
finally {
dlg.dispose();
}
}
private class ServerPasswordPrompt implements Runnable {
private static final String NAME_PREFERENCE = "PasswordPrompt.Name";
private static final String CHOICE_PREFERENCE = "PasswordPrompt.Choice";
private String title;
private String serverType; // label for serverName field
private String serverName;
private NameCallback nameCb;
private PasswordCallback passCb;
private ChoiceCallback choiceCb;
private AnonymousCallback anonymousCb;
private String errorMsg;
private boolean okPressed = false;
ServerPasswordPrompt(String title, String serverType, String serverName,
NameCallback nameCb, PasswordCallback passCb, ChoiceCallback choiceCb,
AnonymousCallback anonymousCb, String errorMsg) {
this.title = title;
this.serverType = serverType;
this.serverName = serverName;
this.nameCb = nameCb;
this.passCb = passCb;
this.choiceCb = choiceCb;
this.anonymousCb = anonymousCb;
this.errorMsg = errorMsg;
}
private String getDefaultUserName() {
if (nameCb == null) {
return ClientUtil.getUserName();
}
return Preferences.getProperty(NAME_PREFERENCE, ClientUtil.getUserName(), true);
}
private int getDefaultChoice() {
try {
String choiceStr = Preferences.getProperty(CHOICE_PREFERENCE);
if (choiceStr != null) {
return Integer.parseInt(choiceStr);
}
}
catch (NumberFormatException e) {
// handled below
}
return 0;
}
@Override
public void run() {
PasswordDialog pwdDialog;
String choicePrompt = null;
String[] choices = null;
if (choiceCb != null) {
choicePrompt = choiceCb.getPrompt();
choices = choiceCb.getChoices();
}
pwdDialog = new PasswordDialog(title, serverType, serverName, passCb.getPrompt(),
nameCb != null ? nameCb.getPrompt() : null, getDefaultUserName(), choicePrompt,
choices, getDefaultChoice(), anonymousCb != null);
if (errorMsg != null) {
pwdDialog.setErrorText(errorMsg);
}
DockingWindowManager winMgr = DockingWindowManager.getActiveInstance();
Component rootFrame = winMgr != null ? winMgr.getRootFrame() : null;
DockingWindowManager.showDialog(rootFrame, pwdDialog);
if (pwdDialog.okWasPressed()) {
if (anonymousCb != null && pwdDialog.anonymousAccessRequested()) {
anonymousCb.setAnonymousAccessRequested(true);
}
else {
passCb.setPassword(pwdDialog.getPassword());
if (nameCb != null) {
String username = pwdDialog.getUserID();
nameCb.setName(username);
Preferences.setProperty(NAME_PREFERENCE, username);
}
if (choiceCb != null) {
int choice = pwdDialog.getChoice();
choiceCb.setSelectedIndex(choice);
Preferences.setProperty(CHOICE_PREFERENCE, Integer.toString(choice));
}
}
okPressed = true;
}
pwdDialog.dispose();
}
boolean okWasPressed() {
return okPressed;
}
}
}

View file

@ -0,0 +1,280 @@
/* ###
* 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.client;
import java.awt.Component;
import java.io.*;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import javax.security.auth.callback.*;
import ghidra.framework.remote.AnonymousCallback;
import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.framework.remote.security.SSHKeyManager;
import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.Msg;
/**
* <code>HeadlessClientAuthenticator</code> provides the ability to install a Ghidra Server
* authenticator needed when operating in a headless mode.
*/
public class HeadlessClientAuthenticator implements ClientAuthenticator {
private final static char[] BADPASSWORD = "".toCharArray();
private static Object sshPrivateKey;
private static String userID = ClientUtil.getUserName(); // default username
private static boolean passwordPromptAlowed;
private Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
Msg.debug(this, "PasswordAuthentication requested for " + getRequestingURL());
String prompt = getRequestingPrompt();
if (prompt == null) {
String host = getRequestingHost();
prompt = (host != null ? (host + " ") : "") + "(" + userID + ") Password:";
}
return new PasswordAuthentication(userID, getPassword(null, prompt));
}
};
HeadlessClientAuthenticator() {
}
@Override
public Authenticator getAuthenticator() {
return authenticator;
}
/**
* Install headless client authenticator for Ghidra Server
* @param username optional username to be used with a Ghidra Server which
* allows username to be specified
* @param keystorePath optional PKI or SSH keystore path. May also be specified
* as resource path for SSH key.
* @param allowPasswordPrompt if true the user may be prompted for passwords
* via the console (stdin). Please note that the Java console will echo
* the password entry to the terminal which may be undesirable.
* @throws IOException if error occurs while opening specified keystorePath
*/
public static void installHeadlessClientAuthenticator(String username, String keystorePath,
boolean allowPasswordPrompt) throws IOException {
passwordPromptAlowed = allowPasswordPrompt;
if (username != null) {
userID = username;
}
// clear existing key store settings
sshPrivateKey = null;
HeadlessClientAuthenticator authenticator = new HeadlessClientAuthenticator();
ClientUtil.setClientAuthenticator(authenticator);
if (keystorePath != null) {
File f = new File(keystorePath);
if (!f.exists()) {
// If keystorePath file not found - try accessing as SSH key resource stream
// InputStream keyIn = ResourceManager.getResourceAsStream(keystorePath);
InputStream keyIn = keystorePath.getClass().getResourceAsStream(keystorePath);
if (keyIn != null) {
try {
sshPrivateKey = SSHKeyManager.getSSHPrivateKey(keyIn);
Msg.info(HeadlessClientAuthenticator.class,
"Loaded SSH key: " + keystorePath);
return;
}
catch (Exception e) {
Msg.error(HeadlessClientAuthenticator.class,
"Failed to open keystore for SSH use: " + keystorePath, e);
throw new IOException("Failed to parse keystore: " + keystorePath);
}
finally {
try {
keyIn.close();
}
catch (IOException e) {
// ignore
}
}
}
Msg.error(HeadlessClientAuthenticator.class, "Keystore not found: " + keystorePath);
throw new FileNotFoundException("Keystore not found: " + keystorePath);
}
try {
sshPrivateKey = SSHKeyManager.getSSHPrivateKey(new File(keystorePath));
Msg.info(HeadlessClientAuthenticator.class, "Loaded SSH key: " + keystorePath);
}
catch (IOException e) {
try {
// try keystore as PKI keystore if failed as SSH keystore
ApplicationKeyManagerFactory.setKeyStore(keystorePath, false);
Msg.info(HeadlessClientAuthenticator.class, "Loaded PKI key: " + keystorePath);
}
catch (IOException e1) {
Msg.error(HeadlessClientAuthenticator.class,
"Failed to open keystore for PKI use: " + keystorePath, e1);
Msg.error(HeadlessClientAuthenticator.class,
"Failed to open keystore for SSH use: " + keystorePath, e);
throw new IOException("Failed to parse keystore: " + keystorePath);
}
}
}
else {
sshPrivateKey = null;
}
}
private char[] getPassword(String usage, String prompt) {
if (!passwordPromptAlowed) {
Msg.warn(this, "Headless client not configured to supply required password");
return BADPASSWORD;
}
char[] password = null;
int c;
try {
String passwordPrompt = "";
if (usage != null) {
passwordPrompt += usage;
passwordPrompt += "\n";
}
if (prompt == null) {
prompt = "Password:";
}
// With the new GhidraClassLoader/GhidraLauncher it should be possible to get a Console
// object, which allow masking of passwords.
Console cons = System.console();
if (cons != null) {
passwordPrompt += prompt + " ";
password = cons.readPassword(passwordPrompt);
}
else {
// Couldn't get console instance, passwords will be in the clear
passwordPrompt += "*** WARNING! Password entry will NOT be masked ***\n" + prompt;
System.out.print(passwordPrompt);
while (true) {
c = System.in.read();
if (c <= 0 || (Character.isWhitespace((char) c) && c != ' ')) {
break;
}
if (password == null) {
password = new char[1];
}
else {
char[] newPass = new char[password.length + 1];
for (int i = 0; i < password.length; i++) {
newPass[i] = password[i];
password[i] = 0;
}
password = newPass;
}
password[password.length - 1] = (char) c;
}
}
}
catch (IOException e) {
Msg.error(this, "Error reading standard-input for password", e);
}
return password;
}
@Override
public char[] getNewPassword(Component parent, String serverInfo, String username) {
throw new UnsupportedOperationException("Server password change not permitted");
}
@Override
public boolean processPasswordCallbacks(String title, String serverType, String serverName,
NameCallback nameCb, PasswordCallback passCb, ChoiceCallback choiceCb,
AnonymousCallback anonymousCb, String loginError) {
if (anonymousCb != null && !passwordPromptAlowed) {
// Assume that login error will not occur with anonymous login
anonymousCb.setAnonymousAccessRequested(true);
return true;
}
if (choiceCb != null) {
choiceCb.setSelectedIndex(1);
}
if (nameCb != null && userID != null) {
nameCb.setName(userID);
}
String usage = null;
if (serverName != null) {
usage = serverType + ": " + serverName;
}
char[] password = getPassword(usage, passCb.getPrompt());
passCb.setPassword(password);
return password != null;
}
@Override
public boolean promptForReconnect(Component parent, String message) {
// assumes connection attempt was immediately done when this
// ClientAuthenticator was installed
return false;
}
@Override
public char[] getKeyStorePassword(String keystorePath, boolean passwordError) {
if (passwordError) {
if (passwordPromptAlowed) {
Msg.error(this, "Incorrect keystore password specified: " + keystorePath);
}
else {
Msg.error(this,
"Keystore password required but password entry has been disabled: " +
keystorePath);
}
return null;
}
return getPassword("Certificate keystore: " + keystorePath, "Keystore password: ");
}
@Override
public boolean processSSHSignatureCallbacks(String serverName, NameCallback nameCb,
SSHSignatureCallback sshCb) {
if (sshPrivateKey == null) {
return false;
}
if (nameCb != null) {
nameCb.setName(userID);
}
try {
sshCb.sign(sshPrivateKey);
return true;
}
catch (IOException e) {
Msg.error(this, "Failed to authenticate with SSH private key", e);
}
return false;
}
@Override
public boolean isSSHKeyAvailable() {
return sshPrivateKey != null;
}
}

View file

@ -0,0 +1,40 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.client;
import java.io.IOException;
/**
* <code>NotConnectedException</code> indicates that the server connection
* is down. When this exception is thrown, the current operation should be
* aborted. At the time this exception is thrown, the user has already been
* informed of a server error condition.
*/
public class NotConnectedException extends IOException {
/**
* Constructor.
* @param msg error message
*/
public NotConnectedException(String msg) {
super(msg);
}
public NotConnectedException(String msg, Throwable cause) {
super(msg, cause);
}
}

View file

@ -0,0 +1,114 @@
/* ###
* 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.client;
import java.awt.Component;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import javax.security.auth.callback.*;
import ghidra.framework.remote.AnonymousCallback;
import ghidra.framework.remote.SSHSignatureCallback;
import ghidra.net.ApplicationKeyManagerFactory;
/**
* <code>PasswordClientAuthenticator</code> provides a fixed username/password
* authentication response when connecting to any Ghidra Server or accessing
* a protected PKI keystore. The use of this authenticator is intended for
* headless applications in which the user is unable to respond to such
* prompts. SSH authentication is not currently supported. Anonymous user
* access is not supported.
* <p>
* If a PKI certificate has been installed, a password may be required
* to access the certificate keystore independent of any other password which may be required
* for accessing SSH keys or server password authentication. In such headless situations,
* the PKI certificate path/password should be specified via a property since it is unlikely
* that the same password will apply.
* @see ApplicationKeyManagerFactory
*/
public class PasswordClientAuthenticator implements ClientAuthenticator {
private char[] password;
private String username;
private Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
};
@Override
public Authenticator getAuthenticator() {
return authenticator;
}
@Override
public boolean isSSHKeyAvailable() {
return false; // does not currently support SSH authentication
}
@Override
public boolean processSSHSignatureCallbacks(String serverName, NameCallback nameCb,
SSHSignatureCallback sshCb) {
return false;
}
public PasswordClientAuthenticator(String password) {
this(null, password);
}
public PasswordClientAuthenticator(String username, String password) {
this.password = password.toCharArray();
this.username = username;
}
@Override
public char[] getNewPassword(Component parent, String serverInfo, String user) {
return null;
}
@Override
public boolean processPasswordCallbacks(String title, String serverType,
String serverName, NameCallback nameCb, PasswordCallback passCb,
ChoiceCallback choiceCb, AnonymousCallback anonymousCb, String loginError) {
if (choiceCb != null) {
choiceCb.setSelectedIndex(1);
}
if (nameCb != null && username != null) {
nameCb.setName(username);
}
passCb.setPassword(password.clone());
return true;
}
@Override
public boolean promptForReconnect(Component parent, String message) {
// assumes connection attempt was immediately done when this
// ClientAuthenticator was installed
return false;
}
@Override
public char[] getKeyStorePassword(String keystorePath, boolean passwordError) {
if (passwordError) {
return null;
}
return password.clone();
}
}

View file

@ -0,0 +1,32 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.client;
/**
* <code>RemoteAdapterListener</code> provides a listener interface
* which facilitates notifcation when the connection
* state of a remote server/repository adapter changes.
*/
public interface RemoteAdapterListener {
/**
* Callback notification indicating the remote object
* connection state has changed.
* @param adapter remote interface adapter (e.g., RepositoryServerAdapter).
*/
void connectionStateChanged(Object adapter);
}

View file

@ -0,0 +1,937 @@
/* ###
* 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.client;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.rmi.*;
import db.buffers.ManagedBufferFileAdapter;
import ghidra.framework.model.ServerInfo;
import ghidra.framework.remote.*;
import ghidra.framework.store.*;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.UserAccessException;
/**
* <code>RepositoryAdapter</code> provides a persistent wrapper for a remote RepositoryHandle
* which may become invalid if the remote connection were to fail. Connection recovery is provided
* by any method call which must communicate with the server.
*/
public class RepositoryAdapter implements RemoteAdapterListener {
private String name;
private RepositoryServerAdapter serverAdapter;
private WeakSet<RemoteAdapterListener> listenerList =
WeakDataStructureFactory.createCopyOnWriteWeakSet();
private RepositoryHandle repository;
private User user;
private boolean unexpectedDisconnect = false;
private boolean noSuchRepository = false;
private volatile int openFileHandleCount = 0;
private boolean ignoreNextOpenFileCountEvent = false;
private RepositoryChangeDispatcher changeDispatcher;
/**
* Construct.
* @param serverAdapter persistent server adapter
* @param name repository name
*/
public RepositoryAdapter(RepositoryServerAdapter serverAdapter, String name) {
this.serverAdapter = serverAdapter;
this.name = name;
changeDispatcher = new RepositoryChangeDispatcher(this);
serverAdapter.addListener(this);
}
/**
* Constructor using a connected repository handle.
* @param serverAdapter persistent server adapter
* @param name repository name
* @param repository connected repository handle.
*/
RepositoryAdapter(RepositoryServerAdapter serverAdapter, String name,
RepositoryHandle repository) {
this(serverAdapter, name);
this.repository = repository;
if (repository != null) {
changeDispatcher.start();
}
}
/**
* Returns true if connection recently was lost unexpectedly
*/
public boolean hadUnexpectedDisconnect() {
return unexpectedDisconnect;
}
@Override
public String toString() {
return serverAdapter.toString() + "(" + name + ")";
}
RepositoryHandle getCurrentHandle() {
return repository;
}
/**
* Set the file system listener associated with the remote repository.
* @param fsListener file system listener
*/
public void setFileSystemListener(FileSystemListener fsListener) {
changeDispatcher.setFileChangeListener(fsListener);
}
/**
* Add a listener to this remote adapter
* @param listener
*/
public void addListener(RemoteAdapterListener listener) {
listenerList.add(listener);
}
/**
* Remove a listener from this remote adapter
* @param listener
*/
public void removeListener(RemoteAdapterListener listener) {
listenerList.remove(listener);
}
/**
* Notify listeners of repository connection state change.
*/
private void fireStateChanged() {
for (RemoteAdapterListener listener : listenerList) {
listener.connectionStateChanged(this);
}
}
/**
* Notification callback when server connection state changes.
* @see ghidra.framework.client.RemoteAdapterListener#connectionStateChanged(java.lang.Object)
*/
@Override
public void connectionStateChanged(Object adapter) {
synchronized (serverAdapter) {
if (!serverAdapter.isConnected()) {
disconnect(serverAdapter.hadUnexpectedDisconnect(), true);
}
else {
try {
connect();
}
catch (IOException e) {
// TODO: handle failed connect?
}
}
}
}
/**
* Returns true if connected.
*/
public boolean isConnected() {
return repository != null;
}
/**
* Attempt to connect to the server.
* @return true if connected
*/
public void connect() throws IOException {
synchronized (serverAdapter) {
if (repository != null) {
try {
repository.getName(); // just called to test the connection.
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return;
}
throw e;
}
}
if (repository == null) {
serverAdapter.connect(); // may cause auto-reconnect of repository
}
if (repository == null) {
repository = serverAdapter.getRepositoryHandle(name);
unexpectedDisconnect = false;
if (repository == null) {
noSuchRepository = true;
throw new IOException("Repository '" + name + "': not found");
}
Msg.info(this, "Connected to repository '" + name + "'");
changeDispatcher.start();
fireStateChanged();
}
}
}
/**
* Event reader for change dispatcher.
* @return
* @throws IOException
* @throws InterruptedIOException if repository handle is closed
*/
RepositoryChangeEvent[] getEvents() throws InterruptedIOException {
RepositoryHandle handle;
synchronized (serverAdapter) {
// Be careful with synchronization since getEvents will block
// until an event occurs
if (repository == null) {
throw new InterruptedIOException();
}
handle = repository;
}
try {
return handle.getEvents();
}
catch (NotConnectedException | RemoteException e) {
// Initiate recover - dispatcher will be restarted
recoverConnection(e);
throw new InterruptedIOException();
}
catch (IOException e) {
synchronized (serverAdapter) {
if (!Thread.currentThread().isInterrupted()) {
serverAdapter.verifyConnection();
disconnect(true, true);
}
}
throw new InterruptedIOException();
}
}
/**
* Returns repository name
*/
public String getName() {
return name;
}
/**
* Returns server adapter
*/
public RepositoryServerAdapter getServer() {
return serverAdapter;
}
/**
* Returns server information
*/
public ServerInfo getServerInfo() {
return serverAdapter.getServerInfo();
}
boolean recoverConnection(IOException e) {
synchronized (serverAdapter) {
if (Thread.currentThread().isInterrupted()) {
return false;
}
// TODO: does exception correspond to a connection or marshaling error?
if (!serverAdapter.verifyConnection()) {
disconnect(serverAdapter.hadUnexpectedDisconnect(), true);
return false;
}
if (noSuchRepository || !(e instanceof NoSuchObjectException)) {
return false;
}
disconnect(true, false);
try {
connect();
}
catch (IOException e1) {
fireStateChanged();
return false;
}
// fireStateChanged(); // force full refresh - NOTE: this could cause a flood of requests if server was bounced
// TODO: without a full refresh lost events could cause a stale view
return true;
}
}
/**
* Returns repository user object.
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryHandle#getUser()
*/
public User getUser() throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
if (user == null) {
user = repository.getUser();
}
return user;
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return getUser();
}
throw e;
}
}
}
/**
* @return true if anonymous access allowed by this repository
* @throws IOException
*/
public boolean anonymousAccessAllowed() throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.anonymousAccessAllowed();
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return anonymousAccessAllowed();
}
throw e;
}
}
}
/**
* Returns list of repository users.
* @throws IOException
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryHandle#getUserList()
*/
public User[] getUserList() throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getUserList();
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getUserList();
}
throw e;
}
}
}
/**
* Returns list of all users known to server.
* @throws IOException
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryHandle#getServerUserList()
*/
public String[] getServerUserList() throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getServerUserList();
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getServerUserList();
}
throw e;
}
}
}
/**
* Set the list of authorized users for this repository.
* @param users list of user and access permissions.
* @param anonymousAccessAllowed true to permit anonymous access (also requires anonymous
* access to be enabled for server)
* @throws UserAccessException
* @throws IOException
* @throws NotConnectedException if server/repository connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryHandle#setUserList(ghidra.framework.remote.User[])
*/
public void setUserList(User[] users, boolean anonymousAccessAllowed) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.setUserList(users, anonymousAccessAllowed);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.setUserList(users, anonymousAccessAllowed);
return;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String, java.lang.String)
*/
public ManagedBufferFileAdapter createDatabase(String parentPath, String itemName,
int bufferSize, String contentType, String fileID, String projectPath)
throws IOException, InvalidNameException {
synchronized (serverAdapter) {
checkRepository();
try {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.createDatabase(parentPath, itemName,
fileID, bufferSize, contentType, projectPath));
fileOpened();
return bf;
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.createDatabase(parentPath,
itemName, fileID, bufferSize, contentType, projectPath));
fileOpened();
return bf;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#openDatabase(java.lang.String, java.lang.String, int)
*/
public ManagedBufferFileAdapter openDatabase(String parentPath, String itemName, int version,
int minChangeDataVer) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.openDatabase(parentPath, itemName,
version, minChangeDataVer));
fileOpened();
return bf;
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.openDatabase(parentPath, itemName,
version, minChangeDataVer));
fileOpened();
return bf;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#openDatabase(java.lang.String, java.lang.String, long)
*/
public ManagedBufferFileAdapter openDatabase(String parentPath, String itemName, long checkoutId)
throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.openDatabase(parentPath, itemName,
checkoutId));
fileOpened();
return bf;
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
ManagedBufferFileAdapter bf =
new ManagedBufferFileAdapter(repository.openDatabase(parentPath, itemName,
checkoutId));
fileOpened();
return bf;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#createDataFile(java.lang.String, java.lang.String)
*/
public void createDataFile(String parentPath, String itemName) throws IOException {
throw new IOException("Data file not yet supported by repository");
}
/*
* @see ghidra.framework.remote.RepositoryHandle#openDataFile(java.lang.String, java.lang.String, int)
*/
public DataFileHandle openDataFile(String parentPath, String itemName, int version)
throws IOException {
throw new IOException("Data file not yet supported by repository");
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getSubfolderList(java.lang.String)
*/
public String[] getSubfolderList(String folderPath) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getSubfolderList(folderPath);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getSubfolderList(folderPath);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getItemCount()
*/
public int getItemCount() throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getItemCount();
}
catch (NotConnectedException | RemoteException e) {
checkUnmarshalException(e, "getItemCount");
if (recoverConnection(e)) {
try {
return repository.getItemCount();
}
catch (RemoteException e1) {
checkUnmarshalException(e1, "getItemCount");
throw e1;
}
}
throw e;
}
}
}
/**
* Convert UnmarshalException into UnsupportedOperationException
* @param e
* @throws UnsupportedOperationException
*/
private void checkUnmarshalException(IOException e, String operation)
throws UnsupportedOperationException {
Throwable t = e.getCause();
if (t instanceof UnmarshalException) {
throw new UnsupportedOperationException(operation);
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getItemList(java.lang.String)
*/
public RepositoryItem[] getItemList(String folderPath) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getItemList(folderPath);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getItemList(folderPath);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getItem(java.lang.String, java.lang.String)
*/
public RepositoryItem getItem(String folderPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getItem(folderPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getItem(folderPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getItem(java.lang.String)
*/
public RepositoryItem getItem(String fileID) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getItem(fileID);
}
catch (NotConnectedException | RemoteException e) {
checkUnmarshalException(e, "getItem by File-ID");
if (recoverConnection(e)) {
try {
return repository.getItem(fileID);
}
catch (RemoteException e1) {
checkUnmarshalException(e1, "getItem by File-ID");
throw e1;
}
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getVersions(java.lang.String, java.lang.String)
*/
public Version[] getVersions(String parentPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getVersions(parentPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getVersions(parentPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#deleteItem(java.lang.String, java.lang.String, int)
*/
public void deleteItem(String parentPath, String itemName, int version) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.deleteItem(parentPath, itemName, version);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.deleteItem(parentPath, itemName, version);
return;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#moveFolder(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
public void moveFolder(String oldParentPath, String newParentPath, String oldFolderName,
String newFolderName) throws InvalidNameException, IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.moveFolder(oldParentPath, newParentPath, oldFolderName, newFolderName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.moveFolder(oldParentPath, newParentPath, oldFolderName,
newFolderName);
return;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#moveItem(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
public void moveItem(String oldParentPath, String newParentPath, String oldItemName,
String newItemName) throws InvalidNameException, IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.moveItem(oldParentPath, newParentPath, oldItemName, newItemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.moveItem(oldParentPath, newParentPath, oldItemName, newItemName);
return;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#checkout(java.lang.String, java.lang.String, ghidra.framework.store.CheckoutType, java.lang.String)
*/
public ItemCheckoutStatus checkout(String folderPath, String itemName,
CheckoutType checkoutType, String projectPath) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.checkout(folderPath, itemName, checkoutType, projectPath);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.checkout(folderPath, itemName, checkoutType, projectPath);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#terminateCheckout(java.lang.String, java.lang.String, long, boolean)
*/
public void terminateCheckout(String folderPath, String itemName, long checkoutId,
boolean notify) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.terminateCheckout(folderPath, itemName, checkoutId, notify);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.terminateCheckout(folderPath, itemName, checkoutId, notify);
return;
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getCheckout(java.lang.String, java.lang.String, long, boolean)
*/
public ItemCheckoutStatus getCheckout(String parentPath, String itemName, long checkoutId)
throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getCheckout(parentPath, itemName, checkoutId);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getCheckout(parentPath, itemName, checkoutId);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getCheckout(java.lang.String, java.lang.String)
*/
public ItemCheckoutStatus[] getCheckouts(String parentPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getCheckouts(parentPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getCheckouts(parentPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#folderExists(java.lang.String)
*/
public boolean folderExists(String folderPath) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.folderExists(folderPath);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.folderExists(folderPath);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#fileExists(java.lang.String, java.lang.String)
*/
public boolean fileExists(String folderPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.fileExists(folderPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.fileExists(folderPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#getLength(java.lang.String, java.lang.String)
*/
public long getLength(String parentPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.getLength(parentPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.getLength(parentPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#hasCheckouts(java.lang.String, java.lang.String)
*/
public boolean hasCheckouts(String parentPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.hasCheckouts(parentPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.hasCheckouts(parentPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#isCheckinActive(java.lang.String, java.lang.String)
*/
public boolean isCheckinActive(String parentPath, String itemName) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
return repository.isCheckinActive(parentPath, itemName);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
return repository.isCheckinActive(parentPath, itemName);
}
throw e;
}
}
}
/*
* @see ghidra.framework.remote.RepositoryHandle#updateCheckoutVersion(java.lang.String, java.lang.String, long, int)
*/
public void updateCheckoutVersion(String parentPath, String itemName, long checkoutId,
int checkoutVersion) throws IOException {
synchronized (serverAdapter) {
checkRepository();
try {
repository.updateCheckoutVersion(parentPath, itemName, checkoutId, checkoutVersion);
}
catch (NotConnectedException | RemoteException e) {
if (recoverConnection(e)) {
repository.updateCheckoutVersion(parentPath, itemName, checkoutId,
checkoutVersion);
return;
}
throw e;
}
}
}
/**
* Verify that the connection is still valid.
* @return true if the connection is valid; false if the connection needs to be reestablished
*/
public boolean verifyConnection() {
if (!serverAdapter.verifyConnection()) {
return false;
}
return true;
}
public void disconnect() {
disconnect(false, true);
}
void disconnect(boolean unexpected, boolean notify) {
synchronized (serverAdapter) {
if (repository != null) {
unexpectedDisconnect = unexpected;
Msg.info(this, "Disconnected from repository '" + name + "'");
changeDispatcher.stop();
try {
repository.close();
}
catch (Throwable t) {
// Failed to close...oh well.
}
repository = null;
user = null;
if (notify) {
fireStateChanged();
}
}
}
}
private void checkRepository() throws NotConnectedException {
if (repository == null) {
throw new NotConnectedException("Not connected to the server");
}
}
private void fileOpened() {
++openFileHandleCount; // force immediate change instead of waiting for delayed update event
ignoreNextOpenFileCountEvent = true; // avoid race condition
}
void processOpenHandleCountUpdateEvent(RepositoryChangeEvent event) {
synchronized (serverAdapter) {
if (ignoreNextOpenFileCountEvent) {
ignoreNextOpenFileCountEvent = false;
return;
}
if (event.type != RepositoryChangeEvent.REP_OPEN_HANDLE_COUNT) {
throw new IllegalArgumentException("Expected REP_OPEN_HANDLE_COUNT event");
}
openFileHandleCount = Integer.parseInt(event.newName);
}
}
public int getOpenFileHandleCount() {
return openFileHandleCount;
}
}

View file

@ -0,0 +1,106 @@
/* ###
* 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.client;
import java.io.InterruptedIOException;
import ghidra.framework.remote.RepositoryChangeEvent;
import ghidra.framework.store.FileSystemListener;
class RepositoryChangeDispatcher implements Runnable {
private FileSystemListener changeListener;
private RepositoryAdapter repAdapter;
private volatile Thread thread;
RepositoryChangeDispatcher(RepositoryAdapter repAdapter) {
this.repAdapter = repAdapter;
}
@Override
public void run() {
try {
while (thread != null) {
processEvents(repAdapter.getEvents());
}
}
catch (InterruptedIOException e) {
// ignore
}
}
public void setFileChangeListener(FileSystemListener changeListener) {
this.changeListener = changeListener;
}
public synchronized void stop() {
if (thread != null) {
thread.interrupt(); // may have no affect on pending RMI call
thread = null;
}
}
public synchronized void start() {
stop();
thread = new Thread(this, "RepChangeDispatcher-" + repAdapter.getName());
thread.setDaemon(true);
thread.start();
}
private void processEvents(RepositoryChangeEvent[] events) {
if (changeListener == null) {
return;
}
for (int i = 0; thread != null && i < events.length; i++) {
RepositoryChangeEvent event = events[i];
switch (event.type) {
case RepositoryChangeEvent.REP_OPEN_HANDLE_COUNT:
repAdapter.processOpenHandleCountUpdateEvent(event);
break;
case RepositoryChangeEvent.REP_FOLDER_CREATED:
changeListener.folderCreated(event.parentPath, event.name);
break;
case RepositoryChangeEvent.REP_FOLDER_DELETED:
changeListener.folderDeleted(event.parentPath, event.name);
break;
case RepositoryChangeEvent.REP_FOLDER_MOVED:
changeListener.folderMoved(event.parentPath, event.name, event.newParentPath);
break;
case RepositoryChangeEvent.REP_FOLDER_RENAMED:
changeListener.folderRenamed(event.parentPath, event.name, event.newName);
break;
case RepositoryChangeEvent.REP_ITEM_CHANGED:
changeListener.itemChanged(event.parentPath, event.name);
break;
case RepositoryChangeEvent.REP_ITEM_CREATED:
changeListener.itemCreated(event.parentPath, event.name);
break;
case RepositoryChangeEvent.REP_ITEM_DELETED:
changeListener.itemDeleted(event.parentPath, event.name);
break;
case RepositoryChangeEvent.REP_ITEM_MOVED:
changeListener.itemMoved(event.parentPath, event.name, event.newParentPath,
event.newName);
break;
case RepositoryChangeEvent.REP_ITEM_RENAMED:
changeListener.itemRenamed(event.parentPath, event.name, event.newName);
break;
}
}
}
}

View file

@ -0,0 +1,565 @@
/* ###
* 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.client;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.rmi.RemoteException;
import java.security.GeneralSecurityException;
import javax.security.auth.login.LoginException;
import docking.widgets.OptionDialog;
import ghidra.framework.model.ServerInfo;
import ghidra.framework.remote.RepositoryHandle;
import ghidra.framework.remote.RepositoryServerHandle;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.*;
/**
* <code>RepositoryServerAdapter</code> provides a persistent wrapper for a
* <code>RepositoryServerHandle</code> which may become invalid if the
* remote connection were to fail.
*/
public class RepositoryServerAdapter {
private static final int HOUR = 60 * 60 * 1000;
private final ServerInfo server;
private final String serverInfoStr;
private String currentUser = ClientUtil.getUserName();
private RepositoryServerHandle serverHandle;
private boolean unexpectedDisconnect = false;
// Keeps track of whether the connection attempt was cancelled by the user
private boolean connectCancelled = false;
private WeakSet<RemoteAdapterListener> listenerList =
WeakDataStructureFactory.createCopyOnWriteWeakSet();
/**
* Construct a repository server interface adapter.
* @param server provides server connection data
*/
RepositoryServerAdapter(ServerInfo server) {
this.server = server;
this.serverInfoStr = server.toString();
}
/**
* Construct a repository server interface adapter.
* @param serverHandle associated server handle (reconnect not supported)
*/
protected RepositoryServerAdapter(RepositoryServerHandle serverHandle,
String serverInfoString) {
this.server = null;
this.serverHandle = serverHandle;
this.serverInfoStr = serverInfoString;
}
@Override
public String toString() {
return serverInfoStr;
}
/**
* Add a listener to this remote adapter
* @param listener
*/
public void addListener(RemoteAdapterListener listener) {
listenerList.add(listener);
}
/**
* Remove a listener from this remote adapter
* @param listener
*/
public void removeListener(RemoteAdapterListener listener) {
listenerList.remove(listener);
}
/**
* Returns true if the connection was cancelled by the user.
*
* @return try if cancelled by user
*/
public boolean isCancelled() {
return connectCancelled;
}
/**
* Notify listeners of a connection state change.
*/
private void fireStateChanged() {
for (RemoteAdapterListener listener : listenerList) {
listener.connectionStateChanged(this);
}
}
/**
* Returns true if connected.
*/
public boolean isConnected() {
return serverHandle != null;
}
/**
* Attempt to connect or re-connect to the server.
* @return true if connect successful, false if cancelled by user
* @throws NotConnectedException if connect failed (error will be displayed to user)
*/
public synchronized boolean connect() throws NotConnectedException {
connectCancelled = false;
if (serverHandle != null) {
if (verifyConnection()) {
return true;
}
}
Throwable cause = null;
try {
serverHandle = ClientUtil.connect(server);
unexpectedDisconnect = false;
if (serverHandle != null) {
Msg.info(this, "Connected to Ghidra Server at " + serverInfoStr);
currentUser = serverHandle.getUser();
fireStateChanged();
checkPasswordExpiration();
return true;
}
// Connect operation cancelled by the user
connectCancelled = true;
return false;
}
catch (LoginException e) {
Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + ").");
cause = e;
}
catch (GeneralSecurityException e) {
Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + "): " + e.getMessage());
cause = e;
}
catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) {
Msg.showError(this, null, "Server Error",
"Connection to server failed (" + server + ").");
cause = e;
}
catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) {
Msg.showError(this, null, "Server Error",
"Server Not Found (" + server.getServerName() + ").");
cause = e;
}
catch (RemoteException e) {
String msg = e.getMessage();
Throwable t = e;
while ((t = t.getCause()) != null) {
String err = t.getMessage();
msg = err != null ? err : t.toString();
cause = t;
}
Msg.showError(this, null, "Server Error",
"An error occured on the server (" + serverInfoStr + ").\n" + msg, e);
}
catch (IOException e) {
String err = e.getMessage();
if (err == null && (e instanceof EOFException)) {
err = "Ghidra Server process may have died";
}
String msg = err != null ? err : e.toString();
Msg.showError(this, null, "Server Error",
"An error occured while connecting to the server (" + serverInfoStr + ").\n" + msg,
e);
}
throw new NotConnectedException("Not connected to repository server", cause);
}
private void checkPasswordExpiration() {
try {
if (!serverHandle.canSetPassword()) {
return;
}
final long expiration = serverHandle.getPasswordExpiration();
if (expiration >= 0) {
String msg;
if (expiration == 0) {
msg = "Your server password has expired!\nPlease change immediately.";
}
else {
long hours = (expiration + HOUR - 1) / HOUR;
msg = "Your password will expire in less than " + hours +
" hour(s)!\nPlease change immediately.";
}
if (SystemUtilities.isInHeadlessMode()) {
Msg.warn(this, msg);
}
else if (OptionDialog.OPTION_ONE == OptionDialog.showOptionDialog(null,
"Password Change Required", msg, "OK", OptionDialog.WARNING_MESSAGE)) { // modal
try {
ClientUtil.changePassword(null, serverHandle, serverInfoStr);
}
catch (IOException e) {
Msg.showError(ServerConnectTask.class, null, "Password Change Failed",
"Password changed failed due to server error!", e);
}
}
}
}
catch (Exception e) {
// getPasswordExpiration method added without changing interface version
// Ignore marshalling error which may occur
}
}
/**
* Returns true if the server handle is already connected
* and functioning properly. A simple remote call is made
* to the handle's connected() method to verify the connection.
*/
synchronized boolean verifyConnection() {
if (serverHandle == null) {
return false;
}
try {
serverHandle.connected();
}
catch (NotConnectedException | RemoteException e) {
if (!recoverConnection(e)) {
return false;
}
}
catch (IOException e) {
return false;
}
return true;
}
private boolean recoverConnection(IOException e) {
if (server == null) {
return false;
}
disconnect(true);
return false;
}
// /**
// * Following an error, this method may be invoked to reestablish
// * the remote connection if needed. If the connection is not
// * down, the RemoteException passed in is simply re-thrown.
// * @param re remote exception which may have been caused by a
// * broken connection.
// * @throws RemoteException re is re-thrown if connection is OK
// * @throws NotConnectedException thrown if connection recovery failed.
// */
// void recover(RemoteException re) throws RemoteException, NotConnectedException {
// if (verifyConnection()) {
//// Err.error(this, null, "Error", "Unexpected Exception: " + re.getMessage(), re);
// throw re;
// }
// serverHandle = null;
// fireStateChanged();
// if (error != null) {
// Err.show(null, "Server Error", "A server communications error occured!", error);
// error = null;
// throw new NotConnectedException("Not connected to repository server");
// }
// connect();
// error = re;
// }
/**
* Create a new repository on the server.
* @param name repository name.
* @return handle to new repository.
* @throws DuplicateNameException
* @throws UserAccessException
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#createRepository(java.lang.String, long)
*/
public synchronized RepositoryAdapter createRepository(String name)
throws DuplicateNameException, UserAccessException, IOException, NotConnectedException {
checkServerHandle();
try {
return new RepositoryAdapter(this, name, serverHandle.createRepository(name));
}
catch (RemoteException e) {
Throwable t = e.getCause();
if (t instanceof DuplicateFileException) {
throw new DuplicateNameException("Repository '" + name + "' already exists");
}
else if (t instanceof UserAccessException) {
throw (UserAccessException) t;
}
if (recoverConnection(e)) {
return new RepositoryAdapter(this, name, serverHandle.createRepository(name));
}
throw e;
}
}
/**
* Get a handle to an existing repository. The repository adapter is
* initially disconnected - the connect() method or another repository
* action method must be invoked to establish a repository connection.
* @param name repository name.
* @return repository handle or null if repository not found.
*/
public RepositoryAdapter getRepository(String name) {
return new RepositoryAdapter(this, name);
}
/**
* Get a handle to an existing repository.
* @param name repository name.
* @return repository handle or null if repository not found.
* @throws UserAccessException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#getRepository(java.lang.String)
*/
synchronized RepositoryHandle getRepositoryHandle(String name)
throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.getRepository(name);
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.getRepository(name);
}
throw e;
}
}
/**
* Delete a repository.
* @param name repository name.
* @throws UserAccessException
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#deleteRepository(java.lang.String)
*/
public synchronized void deleteRepository(String name)
throws UserAccessException, IOException, NotConnectedException {
checkServerHandle();
try {
serverHandle.deleteRepository(name);
}
catch (RemoteException e) {
if (recoverConnection(e)) {
serverHandle.deleteRepository(name);
return;
}
throw e;
}
}
/**
* Returns a list of all repository names defined to the server.
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#getRepositoryNames()
*/
public synchronized String[] getRepositoryNames() throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.getRepositoryNames();
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.getRepositoryNames();
}
throw e;
}
}
/**
* @returns true if server allows anonymous access.
* Individual repositories must grant anonymous access separately.
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#anonymousAccessAllowed()
*/
public synchronized boolean anonymousAccessAllowed() throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.anonymousAccessAllowed();
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.anonymousAccessAllowed();
}
throw e;
}
}
/**
* @returns true if user has restricted read-only access to server (e.g., anonymous user)
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#isReadOnly()
*/
public synchronized boolean isReadOnly() throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.isReadOnly();
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.isReadOnly();
}
throw e;
}
}
/**
* Returns user's server login identity
*/
public String getUser() {
return currentUser;
}
/**
* Returns a list of all known users.
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#getAllUsers()
*/
public synchronized String[] getAllUsers() throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.getAllUsers();
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.getAllUsers();
}
throw e;
}
}
/**
* Set the simple password for the user.
* @param saltedSHA256PasswordHash hex character representation of salted SHA256 hash of the password
* @return true if password changed
* @throws IOException if user data can't be written to file
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#setPassword(char[])
* @see HashUtilities#getSaltedHash("SHA-256", char[])
*/
public synchronized boolean setPassword(char[] saltedSHA256PasswordHash)
throws IOException, NotConnectedException {
checkServerHandle();
try {
return serverHandle.setPassword(saltedSHA256PasswordHash);
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.setPassword(saltedSHA256PasswordHash);
}
throw e;
}
}
/**
* Returns true if this server allows the user to change their password.
* @throws IOException
* @throws NotConnectedException if server connection is down (user already informed)
* @see ghidra.framework.remote.RemoteRepositoryServerHandle#canSetPassword()
*/
public synchronized boolean canSetPassword() {
try {
checkServerHandle();
try {
return serverHandle.canSetPassword();
}
catch (RemoteException e) {
if (recoverConnection(e)) {
return serverHandle.canSetPassword();
}
}
}
catch (IOException e) {
// just return false
}
return false;
}
/**
* Returns the amount of time in milliseconds until the
* user's password will expire.
* @return time until expiration or -1 if it will not expire
* @throws IOException
*/
// public synchronized long getPasswordExpiration() {
// try {
// checkServerHandle();
// try {
// return serverHandle.getPasswordExpiration();
// }
// catch (RemoteException e) {
// disconnect();
// }
// } catch (IOException e) {
// }
// return -1;
// }
/**
* Returns server information. May be null if using fixed RepositoryServerHandle.
*/
public ServerInfo getServerInfo() {
return server;
}
private void checkServerHandle() throws NotConnectedException {
if (serverHandle == null) {
throw new NotConnectedException("Not connected to the server");
}
}
boolean hadUnexpectedDisconnect() {
return unexpectedDisconnect;
}
/**
* Force disconnect with server
*/
public void disconnect() {
disconnect(true);
}
private void disconnect(boolean unexpected) {
if (server == null) {
return; // disconnect/reconnect not supported (Project level URL mechanism)
}
unexpectedDisconnect = unexpected;
Msg.warn(this, "Disconnected from Ghidra Server at " + serverInfoStr);
serverHandle = null;
fireStateChanged();
}
}

View file

@ -0,0 +1,376 @@
/* ###
* 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.client;
import java.io.IOException;
import java.net.*;
import java.net.UnknownHostException;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashSet;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import ghidra.framework.Application;
import ghidra.framework.model.ServerInfo;
import ghidra.framework.remote.*;
import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.Msg;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
* Task for connecting to server with Swing thread.
*/
class ServerConnectTask extends Task {
private ServerInfo server;
//private String defaultUserID;
private boolean allowLoginRetry;
private RemoteRepositoryServerHandle hdl;
private Exception exc;
/**
* Server Connect Task constructor
* @param server server information
* @param allowLoginRetry true if login retry allowed during authentication
*/
ServerConnectTask(ServerInfo server, boolean allowLoginRetry) {
super("Connecting to " + server.getServerName(), false, false, true);
this.server = server;
this.allowLoginRetry = allowLoginRetry;
}
/**
* Completes and necessary authentication and obtains a repository handle.
* If a connection error occurs, an exception will be stored ({@link #getException()}.
* @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/
@Override
public void run(TaskMonitor monitor) {
try {
hdl = getRepositoryServerHandle(ClientUtil.getUserName());
}
catch (RemoteException e) {
exc = e;
Throwable t = e.getCause();
if (t instanceof Exception) {
exc = (Exception) t;
}
}
catch (Exception e) {
exc = e;
}
}
/**
* Returns an IOException, LoginException or RuntimeException
* if handle is null after running task. If both the exception
* and handle are null, it implies the connection attempt was cancelled
* by the user.
*/
Exception getException() {
return exc;
}
/**
* After running the task, this method will return the server handle
* if the connection was successful. If null, a connection error
* may have occurred ({@link #getException()}, or the task was cancelled
* by the user if both the exception and handle are null.
* @return server handle or null if connection attempt failed or was cancelled
*/
RemoteRepositoryServerHandle getRepositoryServerHandle() {
return hdl;
}
private static Subject getLocalUserSubject() {
String username = ClientUtil.getUserName();
HashSet<GhidraPrincipal> pset = new HashSet<>();
HashSet<Object> emptySet = new HashSet<>();
pset.add(new GhidraPrincipal(username));
Subject subj = new Subject(false, pset, emptySet, emptySet);
return subj;
}
private static String getPreferredHostname(String name) {
try {
return InetNameLookup.getCanonicalHostName(name);
}
catch (UnknownHostException e) {
Msg.warn(ServerConnectTask.class, "Failed to resolve hostname for " + name);
}
return name;
}
private static void setOutgoingIpAddress(InetAddress destAddr, int serverPort)
throws IOException {
InetSocketAddress sockAddr = new InetSocketAddress(destAddr, serverPort);
Socket s = new Socket();
s.connect(sockAddr, 5000);
String ip = s.getLocalAddress().getHostAddress();
System.setProperty("java.rmi.server.hostname", ip);
s.close();
}
private static boolean isSSLHandshakeCancelled(SSLHandshakeException e) throws IOException {
if (e.getMessage().indexOf("bad_certificate") > 0) {
if (ApplicationKeyManagerFactory.getPreferredKeyStore() == null) {
throw new IOException("User PKI Certificate not installed", e);
}
// assume user cancelled connect attempt when prompted for cert password
// or other cert error occured
return true;
}
// TODO: Translate SSL exceptions to more meaningful errors
// else if (e.getMessage().indexOf("certificte_unknown") > 0) {
// // cert issued by unrecognized authority
// }
return false;
}
/**
* Obtain a remote instance of the Ghidra Server Handle object
* @param server server information
* @return Ghidra Server Handle object
* @throws IOException
*/
public static GhidraServerHandle getGhidraServerHandle(ServerInfo server) throws IOException {
setOutgoingIpAddress(InetAddress.getByName(server.getServerName()), server.getPortNumber());
Registry reg = LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber());
checkServerBindNames(reg);
GhidraServerHandle gsh = null;
try {
// Test SSL Handshake to ensure that user is able to decrypt keystore.
// This is intended to work around an RMI issue where a continuous
// retry condition can occur when a user cancels the password entry
// for their keystore which should cancel any connection attempt
testServerSSLConnection(server);
gsh = (GhidraServerHandle) reg.lookup(GhidraServerHandle.BIND_NAME);
gsh.checkCompatibility(GhidraServerHandle.INTERFACE_VERSION);
}
catch (NotBoundException e) {
throw new IOException(e.getMessage());
}
catch (SSLHandshakeException e) {
if (isSSLHandshakeCancelled(e)) {
return null;
}
throw e;
}
catch (RemoteException e) {
Throwable cause = e.getCause();
if (cause instanceof UnmarshalException || cause instanceof ClassNotFoundException) {
throw new RemoteException("Incompatible Ghidra Server interface version");
}
if (cause instanceof SSLHandshakeException) {
if (isSSLHandshakeCancelled((SSLHandshakeException) cause)) {
return null;
}
}
throw e;
}
return gsh;
}
/**
* Attempts server connection and completes any necessary authentication.
* @param defaultUserID
* @return server handle or null if authentication was cancelled by user
* @throws IOException
* @throws LoginException
*/
private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID)
throws IOException, LoginException {
GhidraServerHandle gsh = getGhidraServerHandle(server);
if (gsh == null) {
return null;
}
Callback[] callbacks = null;
try {
boolean loopOK = allowLoginRetry;
String loginError = null;
callbacks = gsh.getAuthenticationCallbacks();
SignatureCallback pkiSignatureCb = null;
boolean hasSSHSignatureCallback = false;
if (callbacks != null) {
for (Callback cb : callbacks) {
if (cb instanceof SignatureCallback) {
pkiSignatureCb = (SignatureCallback) cb;
}
else if (cb instanceof SSHSignatureCallback) {
hasSSHSignatureCallback = true;
}
}
}
String serverName = getPreferredHostname(server.getServerName());
AnonymousCallback onlyAnonymousCb = null;
while (true) {
try {
if (callbacks != null) {
if (onlyAnonymousCb != null) {
// First try using no-authentication must have failed -
// go ahead and request anonymous access without asking
onlyAnonymousCb.setAnonymousAccessRequested(true);
loopOK = false; // final try
}
else if (callbacks.length == 1 &&
callbacks[0] instanceof AnonymousCallback) {
// Anonymous option available with No-Authentication mode
// Give no-authentication a chance to work with user-id
// If it fails, a second try will be done using anonymous access
onlyAnonymousCb = (AnonymousCallback) callbacks[0];
loopOK = true;
}
else if (hasSSHSignatureCallback && ClientUtil.isSSHKeyAvailable()) {
// SSH option only available in conjunction with password
// based authentication which will be used if SSH attempt fails
hasSSHSignatureCallback = false; // only try SSH once
ClientUtil.processSSHSignatureCallback(callbacks, serverName,
defaultUserID);
}
else if (pkiSignatureCb != null) {
// when using PKI - no other authentication callback will be used
// if anonymous access allowed, let server validate certificate
// first and assume anonymous access if user unknown but cert is valid
if (!ApplicationKeyManagerFactory.initialize()) {
throw new IOException(
"Client PKI certificate has not been installed");
}
if (ApplicationKeyManagerFactory.usingGeneratedSelfSignedCertificate()) {
Msg.warn(this,
"Server connect - client is using self-signed PKI certificate");
}
loopOK = false; // only try once
ClientUtil.processSignatureCallback(serverName, pkiSignatureCb);
}
else {
// assume all other callback scenarios are password based
// anonymous option must be explicitly chosen over username/password
// when processing password callback
if (!ClientUtil.processPasswordCallbacks(callbacks, serverName,
defaultUserID, loginError)) {
return null; // Cancelled by user
}
}
}
else {
loopOK = false;
}
final RemoteRepositoryServerHandle rsh =
gsh.getRepositoryServer(getLocalUserSubject(), callbacks);
if (rsh.isReadOnly()) {
Msg.showInfo(this, null, "Anonymous Server Login",
"You have been logged-in anonymously to " + serverName +
"\nRead-only permission is granted to repositories which allow anonymous access");
}
return rsh;
}
catch (FailedLoginException e) {
if (loopOK) {
loginError = "Access denied: " + server;
}
else {
throw e;
}
}
}
}
catch (AccessException e) {
throw new IOException(e.getMessage());
}
finally {
if (callbacks != null) {
for (Callback callback : callbacks) {
if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).clearPassword();
}
}
}
}
}
private static void testServerSSLConnection(ServerInfo server) throws IOException {
RMIServerPortFactory portFactory = new RMIServerPortFactory(server.getPortNumber());
SslRMIClientSocketFactory factory = new SslRMIClientSocketFactory();
String serverName = server.getServerName();
int sslRmiPort = portFactory.getRMISSLPort();
try (SSLSocket socket = (SSLSocket) factory.createSocket(serverName, sslRmiPort)) {
// Complete SSL handshake to trigger client keystore access if required
// which will give user ability to cancel without involving RMI which
// will avoid RMI reconnect attempts
socket.startHandshake();
}
}
private static void checkServerBindNames(Registry reg) throws RemoteException {
String requiredVersion = GhidraServerHandle.MIN_GHIDRA_VERSION;
if (!Application.getApplicationVersion().startsWith(requiredVersion)) {
requiredVersion = requiredVersion + " - " + Application.getApplicationVersion();
}
String[] regList = reg.list();
RemoteException exc = null;
int badVerCount = 0;
for (String name : regList) {
if (name.equals(GhidraServerHandle.BIND_NAME)) {
return; // found it
}
else if (name.startsWith(GhidraServerHandle.BIND_NAME_PREFIX)) {
String version = name.substring(GhidraServerHandle.BIND_NAME_PREFIX.length());
if (version.length() == 0) {
version = "4.3.x (or older)";
}
exc = new RemoteException(
"Incompatible Ghidra Server interface, detected interface version " + version +
",\nthis client requires server version " + requiredVersion);
++badVerCount;
}
}
if (exc != null) {
if (badVerCount == 1) {
throw exc;
}
throw new RemoteException("Incompatible Ghidra Server interface, detected " +
badVerCount + " incompatible server versions" +
",\nthis client requires server version " + requiredVersion);
}
throw new RemoteException("Ghidra Server not found.");
}
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body>
Provides classes for connecting to the remote server and accessing the repository.
<br>
</body>
</html>

View file

@ -0,0 +1,83 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.model;
import java.io.Serializable;
/**
* Container for a host name and port number.
*
*/
public class ServerInfo implements Serializable {
private final String host;
private final int portNumber;
/**
* Construct a new ServerInfo object
* @param host host name
* @param portNumber port number
*/
public ServerInfo(String host, int portNumber) {
this.host = host;
this.portNumber = portNumber;
}
/**
* Get the server name.
*/
public String getServerName() {
return host;
}
/**
* Get the port number.
*/
public int getPortNumber() {
return portNumber;
}
/*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ServerInfo)) {
return false;
}
ServerInfo other = (ServerInfo) obj;
return host.equals(other.host) && (portNumber == other.portNumber);
}
/*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return host + ":" + portNumber;
}
/*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return host.hashCode() + portNumber;
}
}

View file

@ -0,0 +1,44 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.remote;
import java.io.Serializable;
import javax.security.auth.callback.Callback;
public class AnonymousCallback implements Callback, Serializable {
public static final long serialVersionUID = 1L;
private boolean anonymousAccessRequested = false;
/**
* If state set to true anonymous read-only access will be requested
* @param state true to request anonymous access
*/
public void setAnonymousAccessRequested(boolean state) {
anonymousAccessRequested = state;
}
/**
* @returns true if anonymous access requested
*/
public boolean anonymousAccessRequested() {
return anonymousAccessRequested;
}
}

View file

@ -0,0 +1,66 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.remote;
import java.security.Principal;
import java.util.Set;
import javax.security.auth.Subject;
/**
* <code>GhidraPrincipal</code> specifies a Ghidra user as a Principal
* for use with server login/authentication.
*/
public class GhidraPrincipal implements Principal, java.io.Serializable {
public final static long serialVersionUID = 1L;
private String username;
/**
* Constructor.
* @param username user id/name
*/
public GhidraPrincipal(String username) {
this.username = username;
}
/*
* @see java.security.Principal#getName()
*/
public String getName() {
return username;
}
/**
* Returns the GhidraPrincipal object contained within a Subject, or null if
* not found.
*
* @param subj user subject
* @return GhidraPrincipal or null
*/
public static GhidraPrincipal getGhidraPrincipal(Subject subj) {
if (subj != null) {
Set<GhidraPrincipal> set = subj.getPrincipals(GhidraPrincipal.class);
if (!set.isEmpty()) {
return set.iterator().next();
}
}
return null;
}
}

View file

@ -0,0 +1,102 @@
/* ###
* 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.remote;
import java.rmi.Remote;
import java.rmi.RemoteException;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.login.LoginException;
/**
* <code>GhidraServerHandle</code> provides access to a remote server.
* This remote interface facilitates user login/authentication, providing
* a more useful handle to the associated repository server.
*/
public interface GhidraServerHandle extends Remote {
/**
* The collective interface version for all Ghidra Server remote interfaces.
* If any remote interface is modified, this value should be incremented.
*
* Version Change History:
* 1: Original Version
* 2: Changed API to support NAT and firewalls
* 3: Allow user to login with alternate user ID
* 4: Added additional checkout data and database ID support (4.2)
* 5: Added support for quick update of checkout file following merged check-in on server,
* also added alternate authentication via password file (4.4)
* 6: Refactored BufferFile related classes creating a ManagedBufferFile which
* supports all the version-control capabilities. (5.2)
* 7: Added support for SSH authentication callback, anonymous user access (5.4)
* 8: Added salted local passwords, added LocalIndexedFilesystem V1 with ability to obtain file count (6.1)
* 9: Added support for transient checkouts (7.2)
* 10: Added BlockStreamServer (7.4)
* 11: Revised password hash to SHA-256 (9.0)
*/
public static final int INTERFACE_VERSION = 11;
/**
* Minimum version of Ghidra which utilized the current INTERFACE_VERSION
*/
public static final String MIN_GHIDRA_VERSION = "9.0";
/**
* Default RMI base port for Ghidra Server
*/
static final int DEFAULT_PORT = 13100;
/**
* RMI registry binding name prefix for all versions of the remote GhidraServerHandle object.
*/
static final String BIND_NAME_PREFIX = "GhidraServer";
/**
* RMI registry binding name for the supported version of the remote GhidraServerHandle object.
*/
static final String BIND_NAME = BIND_NAME_PREFIX + MIN_GHIDRA_VERSION;
/**
* Returns user authentication proxy object.
* @throws RemoteException
* @return authentication callbacks which must be satisfied or null if authentication not
* required.
*/
Callback[] getAuthenticationCallbacks() throws RemoteException;
/**
* Get a handle to the repository server.
* @param user user subject containing GhidraPrincipal
* @param authCallbacks valid authentication callback objects which have been satisfied, or
* null if server does not require authentication.
* @return repository server handle.
* @throws LoginException if user authentication fails
* @throws RemoteException
* @see #getAuthenticationCallbacks()
*/
RemoteRepositoryServerHandle getRepositoryServer(Subject user, Callback[] authCallbacks)
throws LoginException, RemoteException;
/**
* Check server interface compatibility
* @param serverInterfaceVersion client/server interface version
* @throws RemoteException
* @see #INTERFACE_VERSION
*/
void checkCompatibility(int serverInterfaceVersion) throws RemoteException;
}

View file

@ -0,0 +1,97 @@
/* ###
* 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.remote;
import java.net.InetAddress;
import java.net.UnknownHostException;
import ghidra.util.Msg;
public class InetNameLookup {
private static final long MAX_TIME_MS = 10000;
private static volatile boolean lookupEnabled = true;
private static volatile boolean disableOnFailure = false;
private InetNameLookup() {
// static use only
}
public static void setDisableOnFailure(boolean state) {
disableOnFailure = state;
}
public static void setLookupEnabled(boolean enable) {
lookupEnabled = enable;
}
public static boolean isEnabled() {
return lookupEnabled;
}
/**
* Gets the fully qualified domain name for this IP address or hostname.
* Best effort method, meaning we may not be able to return
* the FQDN depending on the underlying system configuration.
*
* @param host IP address or hostname
*
* @return the fully qualified domain name for this IP address,
* or if the operation is not allowed/fails
* the original host name specified.
*
* @throws UnknownHostException the forward lookup of the specified address
* failed
*/
public static String getCanonicalHostName(String host) throws UnknownHostException {
String bestGuess = host;
if (lookupEnabled) {
// host may have multiple IP addresses
boolean found = false;
long fastest = Long.MAX_VALUE;
for (InetAddress addr : InetAddress.getAllByName(host)) {
long startTime = System.currentTimeMillis();
String name = addr.getCanonicalHostName();
long elapsedTime = System.currentTimeMillis() - startTime;
if (!name.equals(addr.getHostAddress())) {
if (host.equalsIgnoreCase(name)) {
return name; // name found matches original - use it
}
bestGuess = name; // name found - update best guess
found = true;
}
else {
// keep fastest reverse lookup time
fastest = Math.min(fastest, elapsedTime);
}
}
if (!found) {
// if lookup failed to produce a name - log warning
Msg.warn(InetNameLookup.class, "Failed to resolve IP Address: " + host +
" (Reverse DNS may not be properly configured or you may have a network problem)");
if (disableOnFailure && fastest > MAX_TIME_MS) {
// if lookup failed and was slow - disable future lookups if disableOnFailure is true
Msg.warn(InetNameLookup.class,
"Reverse network name lookup has been disabled automatically due to lookup failure.");
lookupEnabled = false;
}
}
}
return bestGuess;
}
}

View file

@ -0,0 +1,53 @@
/* ###
* 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.remote;
public class RMIServerPortFactory {
private static final int REGISTERY_PORT = 0;
private static final int RMI_SSL_PORT = 1;
private static final int STREAM_PORT = 2;
private int basePort;
/**
* Construct port factory using specified basePort
*/
public RMIServerPortFactory(int basePort) {
this.basePort = basePort;
}
/**
* Returns RMI Registry port
*/
public int getRMIRegistryPort() {
return basePort + REGISTERY_PORT;
}
/**
* Returns the SSL-protected RMI port.
*/
public int getRMISSLPort() {
return basePort + RMI_SSL_PORT;
}
/**
* Returns the SSL Stream port
*/
public int getStreamPort() {
return basePort + STREAM_PORT;
}
}

View file

@ -0,0 +1,25 @@
/* ###
* 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.remote;
import java.rmi.Remote;
/**
* <code>RepositoryHandle</code> provides access to a remote repository via RMI.
*/
public interface RemoteRepositoryHandle extends RepositoryHandle, Remote {
}

View file

@ -0,0 +1,25 @@
/* ###
* 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.remote;
import java.rmi.Remote;
/**
* <code>RepositoryServerHandle</code> provides access to a remote repository server via RMI.
*/
public interface RemoteRepositoryServerHandle extends RepositoryServerHandle, Remote {
}

View file

@ -0,0 +1,96 @@
/* ###
* 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.remote;
import java.io.Serializable;
/**
* Repository change event (used by server only).
*/
public class RepositoryChangeEvent implements Serializable {
public final static long serialVersionUID = 1L;
public static final int REP_NULL_EVENT = -1;
public static final int REP_FOLDER_CREATED = 0;
public static final int REP_ITEM_CREATED = 1;
public static final int REP_FOLDER_DELETED = 2;
public static final int REP_FOLDER_MOVED = 3;
public static final int REP_FOLDER_RENAMED = 4;
public static final int REP_ITEM_DELETED = 5;
public static final int REP_ITEM_RENAMED = 6;
public static final int REP_ITEM_MOVED = 7;
public static final int REP_ITEM_CHANGED = 8;
public static final int REP_OPEN_HANDLE_COUNT = 9;
private static final int LAST_TYPE = REP_OPEN_HANDLE_COUNT;
private static final String[] TYPES = new String[] { "Folder Created", "Item Created",
"Folder Deleted", "Folder Moved", "Folder Renamed", "Item Deleted", "Item Renamed",
"Item Moved", "Item Changed", "Open Handle Cnt" };
public final int type;
public final String parentPath;
public final String name;
public final String newParentPath;
public final String newName;
/**
* Constructor.
* Parameters not applicable to the specified type may be null.
* @param type event type
* @param parentPath parent folder path for repository item or folder
* @param name repository item or folder name
* @param newParentPath new parent folder path for repository item or folder
* @param newName new repository item or folder name
*/
public RepositoryChangeEvent(int type, String parentPath, String name, String newParentPath,
String newName) {
this.type = type;
this.parentPath = parentPath;
this.name = name;
this.newParentPath = newParentPath;
this.newName = newName;
}
/*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
if (type >= 0 && type <= LAST_TYPE) {
StringBuffer buf = new StringBuffer();
buf.append("<");
buf.append(TYPES[type]);
buf.append(",parentPath=");
buf.append(parentPath);
buf.append(",name=");
buf.append(name);
buf.append(",newParentPath=");
buf.append(newParentPath);
buf.append(",newName=");
buf.append(newName);
buf.append(">");
return buf.toString();
}
else if (type == REP_NULL_EVENT) {
return "<Null Event>";
}
return "<Unknown RepositoryChangeEvent>";
}
}

View file

@ -0,0 +1,327 @@
/* ###
* 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.remote;
import java.io.FileNotFoundException;
import java.io.IOException;
import db.buffers.ManagedBufferFileHandle;
import ghidra.framework.store.*;
import ghidra.util.InvalidNameException;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.exception.UserAccessException;
/**
* <code>RepositoryHandle</code> provides access to a repository.
*/
public interface RepositoryHandle {
// TODO: NOTE! Debugging client or sever garbage collection delays could
// cause handle to be disposed prematurely.
public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 1000 : 30000;
/**
* Returns the name of this repository.
* @throws IOException if an IO error occurs
*/
String getName() throws IOException;
/**
* Returns user object associated with this handle.
* @throws IOException if an IO error occurs
*/
User getUser() throws IOException;
/**
* Returns a list of users authorized for this repository.
* @throws UserAccessException
* @throws IOException if an IO error occurs
*/
User[] getUserList() throws IOException;
/**
* @return true if anonymous access allowed by this repository
* @throws IOException if an IO error occurs
*/
boolean anonymousAccessAllowed() throws IOException;
/**
* Convenience method for obtaining a list of all users
* known to the server.
* @return list of user names.
* @throws IOException if an IO error occurs
* @see RemoteRepositoryServerHandle#getAllUsers
*/
String[] getServerUserList() throws IOException;
/**
* Set the list of authorized users for this repository.
* @param users list of user and access permissions.
* @param anonymousAccessAllowed true if anonymous access should be permitted to
* this repository
* @throws UserAccessException
* @throws IOException if an IO error occurs
*/
void setUserList(User[] users, boolean anonymousAccessAllowed) throws IOException;
/**
* Get list of subfolders contained within the specified parent folder.
* @param folderPath parent folder path
* @return list of subfolder names
* @throws UserAccessException if user does not have adequate permission within the repository.
* @throws FileNotFoundException if specified parent folder path not found
* @throws IOException if an IO error occurs
*/
String[] getSubfolderList(String folderPath) throws IOException;
/**
* Returns the number of folder items contained within this file-system.
* @throws IOException if an IO error occurs
* @throws UnsupportedOperationException if file-system does not support this operation
*/
int getItemCount() throws IOException;
/**
* Get of all items found within the specified parent folder path.
* @param folderPath parent folder path
* @return list of items contained within specified parent folder
* @throws UserAccessException
* @throws FileNotFoundException if parent folder not found
* @throws IOException if an IO error occurs
*/
RepositoryItem[] getItemList(String folderPath) throws IOException;
/**
* Returns the RepositoryItem in the given folder with the given name
* @param parentPath folder path
* @param name item name
* @return item or null if not found
* @throws IOException if an IO error occurs
*/
RepositoryItem getItem(String parentPath, String name) throws IOException;
/**
* Returns the RepositoryItem with the given unique file ID
* @param fileID unique file ID
* @return item or null if not found
* @throws IOException if an IO error occurs
* @throws UnsupportedOperationException if file-system does not support this operation
*/
RepositoryItem getItem(String fileID) throws IOException;
/**
* Create a new empty database item within the repository.
* @param parentPath parent folder path
* @param itemName new item name
* @param fileID unique file ID
* @param bufferSize buffer file buffer size
* @param contentType application content type
* @param projectPath path of user's project
* @return initial buffer file open for writing
* @throws UserAccessException if user does not have adequate permission within the repository.
* @throws DuplicateFileException item path already exists within repository
* @throws IOException if an IO error occurs
* @throws InvalidNameException if itemName or parentPath contains invalid characters
*/
ManagedBufferFileHandle createDatabase(String parentPath, String itemName, String fileID,
int bufferSize, String contentType, String projectPath) throws IOException,
InvalidNameException;
/**
* Open an existing version of a database buffer file for non-update read-only use.
* @param parentPath parent folder path
* @param itemName name of existing data file
* @param version existing version of data file (-1 = latest version)
* @param minChangeDataVer indicates the oldest change data buffer file to be
* included. A -1 indicates only the last change data buffer file is applicable.
* @return remote buffer file for non-update read-only use
* @throws UserAccessException if user does not have adequate permission within the repository.
* @throws FileNotFoundException if database version not found
* @throws IOException if an IO error occurs
*/
ManagedBufferFileHandle openDatabase(String parentPath, String itemName, int version,
int minChangeDataVer) throws IOException;
/**
* Open the current version for checkin of new version.
* @param parentPath parent folder path
* @param itemName name of existing data file
* @param checkoutId checkout ID
* @return remote buffer file for updateable read-only use
* @throws UserAccessException if user does not have adequate permission within the repository.
* @throws FileNotFoundException if database version not found
* @throws IOException if an IO error occurs
*/
ManagedBufferFileHandle openDatabase(String parentPath, String itemName, long checkoutId)
throws IOException;
/**
* Returns a list of all versions for the specified item.
* @param parentPath parent folder path
* @param itemName name of item
* @return version list
* @throws IOException if an IO error occurs
*/
Version[] getVersions(String parentPath, String itemName) throws IOException;
/**
* Delete the specified version of an item.
* @param parentPath parent folder path
* @param itemName name of item
* @param version oldest or latest version of item to be deleted, or -1
* to delete the entire item. User must be Admin or owner of version to be
* deleted.
* @throws IOException if an IO error occurs
*/
void deleteItem(String parentPath, String itemName, int version) throws IOException;
/**
* Move an entire folder
* @param oldParentPath current parent folder path
* @param newParentPath new parent folder path
* @param oldFolderName current folder name
* @param newFolderName new folder name
* @throws InvalidNameException if newFolderName is invalid
* @throws DuplicateFileException if target folder already exists
* @throws IOException if an IO error occurs
*/
void moveFolder(String oldParentPath, String newParentPath, String oldFolderName,
String newFolderName) throws InvalidNameException, IOException;
/**
* Move an item to another folder
* @param oldParentPath current parent folder path
* @param newParentPath new parent folder path
* @param oldItemName current item name
* @param newItemName new item name
* @throws InvalidNameException if newItemName is invalid
* @throws DuplicateFileException if target item already exists
* @throws IOException if an IO error occurs
*/
void moveItem(String oldParentPath, String newParentPath, String oldItemName, String newItemName)
throws InvalidNameException, IOException;
/**
* Perform a checkout on the specified item.
* @param parentPath parent folder path
* @param itemName name of item
* @param CheckoutType checkout type. If exclusive or transient, checkout is only successful
* if no other checkouts exist. No new checkouts of item will be permitted while an
* exclusive/transient checkout is active.
* @param projectPath path of user's project
* @return checkout data
* @throws IOException if an IO error occurs
*/
ItemCheckoutStatus checkout(String parentPath, String itemName, CheckoutType checkoutType,
String projectPath) throws IOException;
/**
* Terminate an existing item checkout.
* @param parentPath parent folder path
* @param itemName name of item
* @param checkoutId checkout ID
* @param notify notify listeners of item status change
* @throws IOException if an IO error occurs
*/
void terminateCheckout(String parentPath, String itemName, long checkoutId, boolean notify)
throws IOException;
/**
* Returns specific checkout data for an item.
* @param parentPath parent folder path
* @param itemName name of item
* @param checkoutId checkout ID
* @return checkout data
* @throws IOException if an IO error occurs
*/
ItemCheckoutStatus getCheckout(String parentPath, String itemName, long checkoutId)
throws IOException;
/**
* Get a list of all checkouts for an item.
* @param parentPath parent folder path
* @param itemName name of item
* @return checkout data list
* @throws IOException if an IO error occurs
*/
ItemCheckoutStatus[] getCheckouts(String parentPath, String itemName) throws IOException;
/**
* Returns true if the specified folder path exists.
* @param folderPath folder path
* @throws IOException if an IO error occurs
*/
boolean folderExists(String folderPath) throws IOException;
/**
* Returns true if the specified item exists.
* @param parentPath parent folder path
* @param itemName name of item
* @throws IOException if an IO error occurs
*/
boolean fileExists(String parentPath, String itemName) throws IOException;
/**
* Returns the length of this domain file. This size is the minimum disk space
* used for storing this file, but does not account for additional storage space
* used to tracks changes, etc.
* @param parentPath parent folder path
* @param itemName name of item
* @return file length
* @throws IOException if an IO error occurs
*/
long getLength(String parentPath, String itemName) throws IOException;
/**
* Returns true if the specified item has one or more checkouts.
* @param parentPath parent folder path
* @param itemName name of item
*/
boolean hasCheckouts(String parentPath, String itemName) throws IOException;
/**
* Returns true if the specified item has an active checkin.
* @param parentPath parent folder path
* @param itemName name of item
*/
boolean isCheckinActive(String parentPath, String itemName) throws IOException;
/**
* Update checkout data for an item following an update of a local checkout file.
* @param parentPath parent folder path
* @param itemName name of item
* @param checkoutId checkout ID
* @param checkoutVersion item version used for update
* @throws IOException if error occurs
*/
void updateCheckoutVersion(String parentPath, String itemName, long checkoutId,
int checkoutVersion) throws IOException;
/**
* Get pending change events. Call will block until an event is available.
* @return array of events
* @throws IOException if error occurs.
*/
RepositoryChangeEvent[] getEvents() throws IOException;
/**
* Notification to server that client is dropping handle.
* @throws IOException if error occurs
*/
void close() throws IOException;
}

View file

@ -0,0 +1,165 @@
/* ###
* 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.remote;
import ghidra.framework.store.FileSystem;
import java.io.IOException;
/**
* <code>RepositoryItemStatus</code> provides status information for a
* repository folder item.
*/
public class RepositoryItem implements java.io.Serializable {
public final static long serialVersionUID = 2L;
public final static int FILE = 1;
public final static int DATABASE = 2;
protected String folderPath;
protected String itemName;
protected String fileID;
protected int itemType;
protected String contentType;
protected int version;
protected long versionTime;
/**
* Default constructor needed for de-serialization
*/
protected RepositoryItem() {
}
/**
* Constructor.
* @param folderPath path of folder containing item.
* @param itemName name of item
* @param itemType type of item (FILE or DATABASE)
* @param contentType content type associated with item
* @param version repository item version or -1 if versioning not supported
* @param versionTime version creation time
* @param checkoutList list of checkouts for the associated repository item.
*/
public RepositoryItem(String folderPath, String itemName, String fileID, int itemType,
String contentType, int version, long versionTime) {
this.folderPath = folderPath;
this.itemName = itemName;
this.fileID = fileID;
this.itemType = itemType;
this.contentType = contentType;
this.version = version;
this.versionTime = versionTime;
}
/**
* Serialization method
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.writeLong(serialVersionUID);
out.writeUTF(folderPath);
out.writeUTF(itemName);
out.writeUTF(fileID != null ? fileID : "");
out.writeInt(itemType);
out.writeUTF(contentType != null ? contentType : "");
out.writeInt(version);
out.writeLong(versionTime);
}
/**
* Deserialization method
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
long serialVersion = in.readLong();
if (serialVersion != serialVersionUID) {
throw new ClassNotFoundException("Unsupported version of RepositoryItemStatus");
}
folderPath = in.readUTF();
itemName = in.readUTF();
fileID = in.readUTF();
if (fileID.length() == 0) {
fileID = null;
}
itemType = in.readInt();
contentType = in.readUTF();
if (contentType.length() == 0) {
contentType = null;
}
version = in.readInt();
versionTime = in.readLong();
}
/**
* Returns the item name.
*/
public String getName() {
return itemName;
}
/**
* Returns the folder item path within the repository.
*/
public String getPathName() {
return folderPath + FileSystem.SEPARATOR + itemName;
}
/**
* Returns path of the parent folder containing this item.
*/
public String getParentPath() {
return folderPath;
}
/**
* Returns type of item.
*/
public int getItemType() {
return itemType;
}
/**
* Returns content class
*/
public String getContentType() {
return contentType;
}
public String getFileID() {
return fileID;
}
/**
* Returns the current version of the item or
* -1 if versioning not supported.
*/
public int getVersion() {
return version;
}
/**
* Returns the time (UTC milliseconds) when the current version was created.
*/
public long getVersionTime() {
return versionTime;
}
}

View file

@ -0,0 +1,117 @@
/* ###
* 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.remote;
import java.io.IOException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.exception.UserAccessException;
/**
* <code>RepositoryServerHandle</code> provides access to a repository server.
*/
public interface RepositoryServerHandle {
/**
* @returns true if server allows anonymous access.
* Individual repositories must grant anonymous access separately.
* @throws IOException if an IO error occurs
*/
boolean anonymousAccessAllowed() throws IOException;
/**
* @returns true if user has restricted read-only access to server (e.g., anonymous user)
* @throws IOException if an IO error occurs
*/
boolean isReadOnly() throws IOException;
/**
* Create a new repository on the server. The newly created RepositoryHandle will contain
* a unique project ID for the client.
* @param name repository name.
* This ID will be used to identify and maintain checkout data.
* @return handle to new repository.
* @throws DuplicateFileException
* @throws UserAccessException
* @throws IOException if an IO error occurs
*/
RepositoryHandle createRepository(String name) throws IOException;
/**
* Get a handle to an existing repository.
* @param name repository name.
* @return repository handle or null if repository does not exist.
* @throws UserAccessException if user does not have permission to access repository
* @throws IOException if an IO error occurs
*/
RepositoryHandle getRepository(String name) throws IOException;
/**
* Delete a repository.
* @param name repository name.
* @throws UserAccessException if user does not have permission to delete repository
* @throws IOException if an IO error occurs
*/
void deleteRepository(String name) throws IOException;
/**
* Returns a list of all repository names which are accessable by the current user.
* @throws IOException if an IO error occurs
*/
String[] getRepositoryNames() throws IOException;
/**
* Returns current user for which this handle belongs.
* @throws IOException if an IO error occurs
*/
String getUser() throws IOException;
/**
* Returns a list of all known users.
* @throws IOException if an IO error occurs
*/
String[] getAllUsers() throws IOException;
/**
* Returns true if the user's password can be changed.
* @throws IOException if an IO error occurs
*/
boolean canSetPassword() throws IOException;
/**
* Returns the amount of time in milliseconds until the
* user's password will expire.
* @return time until expiration or -1 if it will not expire
* @throws IOException if an IO error occurs
*/
long getPasswordExpiration() throws IOException;
/**
* Set the password for the user.
* @param saltedSHA256PasswordHash SHA256 salted password hash
* @returns true if password changed
* @throws IOException if an IO error occurs
* @see HashUtilities#getSaltedHash("SHA-256", char[])
*/
boolean setPassword(char[] saltedSHA256PasswordHash) throws IOException;
/**
* Verify that server is alive and connected.
* @throws IOException if connection verification fails
*/
void connected() throws IOException;
}

View file

@ -0,0 +1,108 @@
/* ###
* 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.remote;
import java.io.IOException;
import java.io.Serializable;
import java.security.SecureRandom;
import javax.security.auth.callback.Callback;
import ch.ethz.ssh2.signature.*;
import generic.random.SecureRandomFactory;
/**
* <code>SSHSignatureCallback</code> provides a Callback implementation used
* to perform SSH authentication. This callback is instantiated
* by the server with a random token which must be signed using the
* user's SSH private key.
* <p>
* It is the responsibility of the callback handler to invoke the
* sign method and return this object in response
* to the callback.
*/
public class SSHSignatureCallback implements Callback, Serializable {
public static final long serialVersionUID = 1L;
private final byte[] token;
private final byte[] serverSignature;
private byte[] signature;
/**
* Construct callback with a random token to be signed by the client.
* @param token random bytes to be signed
*/
public SSHSignatureCallback(byte[] token, byte[] serverSignature) {
this.token = token;
this.serverSignature = serverSignature;
}
/**
* @returns token to be signed using user certificate.
*/
public byte[] getToken() {
return (token == null ? null : (byte[]) token.clone());
}
/**
* @returns signed token bytes set by callback handler.
*/
public byte[] getSignature() {
return (signature == null ? null : (byte[]) signature.clone());
}
/**
* @returns the server's signature of the token bytes.
*/
public byte[] getServerSignature() {
return serverSignature;
}
/**
* @returns true if callback has been signed
*/
public boolean isSigned() {
return signature != null;
}
/**
* Sign this challenge with the specified SSH private key.
* @param sshPrivateKey RSAPrivateKey or DSAPrivateKey
* @throws IOException if signature generation failed
* @see RSAPrivateKey
* @see DSAPrivateKey
*/
public void sign(Object sshPrivateKey) throws IOException {
if (sshPrivateKey instanceof RSAPrivateKey) {
RSAPrivateKey key = (RSAPrivateKey) sshPrivateKey;
// TODO: verify correct key by using accepted public key fingerprint
RSASignature rsaSignature = RSASHA1Verify.generateSignature(token, key);
signature = RSASHA1Verify.encodeSSHRSASignature(rsaSignature);
}
else if (sshPrivateKey instanceof DSAPrivateKey) {
DSAPrivateKey key = (DSAPrivateKey) sshPrivateKey;
// TODO: verify correct key by using accepted public key fingerprint
SecureRandom random = SecureRandomFactory.getSecureRandom();
DSASignature dsaSignature = DSASHA1Verify.generateSignature(token, key, random);
signature = DSASHA1Verify.encodeSSHDSASignature(dsaSignature);
}
else {
throw new IllegalArgumentException("Unsupported SSH private key");
}
}
}

View file

@ -0,0 +1,147 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.remote;
import java.io.Serializable;
import java.security.Principal;
import java.security.cert.X509Certificate;
import javax.security.auth.callback.Callback;
import javax.security.auth.x500.X500Principal;
/**
* <code>SignatureCallback</code> provides a Callback implementation used
* to perform PKI authentication. This callback is instantiated
* by the server with a random token which must be signed using the
* user's certificate which contains one of the recognizedAuthorities
* within it certificate chain.
* <p>
* It is the responsibility of the callback handler to invoke the
* sign(X509Certificate[], byte[]) and return this object in response
* to the callback.
*/
public class SignatureCallback implements Callback, Serializable {
public static final long serialVersionUID = 1L;
private X500Principal[] recognizedAuthorities;
private final byte[] token;
private final byte[] serverSignature;
private byte[] signature;
private X509Certificate[] certChain;
/**
* Construct callback with a random token to be signed by the client.
* @param recognizedAuthorities list of CA's from which one must occur
* within the certificate chain of the signing certificate.
* @param token random bytes to be signed
*/
public SignatureCallback(X500Principal[] recognizedAuthorities, byte[] token,
byte[] serverSignature) {
this.token = token;
this.serverSignature = serverSignature;
this.recognizedAuthorities = recognizedAuthorities;
}
/**
* Returns list of approved certificate authorities.
*/
public Principal[] getRecognizedAuthorities() {
return (recognizedAuthorities == null ? null : (Principal[]) recognizedAuthorities.clone());
}
/**
* Returns token to be signed using user certificate.
*/
public byte[] getToken() {
return (token == null ? null : (byte[]) token.clone());
}
/**
* Returns signed token bytes set by callback handler.
*/
public byte[] getSignature() {
return (signature == null ? null : (byte[]) signature.clone());
}
/**
* Returns the server's signature of the token bytes.
*/
public byte[] getServerSignature() {
return serverSignature;
}
/**
* Returns certificate chain used to sign token.
*/
public X509Certificate[] getCertificateChain() {
return certChain;
}
/**
* Set token signature data. Method must be invoked by
* callback handler.
* @param sigCertChain certificate chain used to sign token.
* @param certSignature token signature
*/
public void sign(X509Certificate[] sigCertChain, byte[] certSignature) {
this.certChain = sigCertChain;
this.signature = (certSignature == null ? null : certSignature.clone());
}
public String getSigAlg() {
// TODO Auto-generated method stub
return null;
}
// private void writeObject(java.io.ObjectOutputStream out) throws IOException {
//
// out.defaultWriteObject();
//
// try {
// out.writeInt(certChain == null ? -1 : certChain.length);
// if (certChain != null) {
// for (int i = 0; i < certChain.length; i++) {
// out.writeObject(certChain[i].getEncoded());
// }
// }
// } catch (CertificateEncodingException e) {
// throw new IOException("Can not serialize certificate chain");
// }
// }
//
// private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//
// in.defaultReadObject();
//
// try {
// int cnt = in.readInt();
// if (cnt >= 0) {
// CertificateFactory cf = CertificateFactory.getInstance("X509");
// certChain = new X509Certificate[cnt];
// for (int i = 0; i < cnt; i++) {
// byte[] bytes = (byte[]) in.readObject();
// certChain[i] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
// }
// }
// } catch (CertificateException e) {
// throw new IOException("Can not de-serialize certificate chain");
// }
// }
}

View file

@ -0,0 +1,159 @@
/* ###
* 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.remote;
import java.io.Serializable;
/**
* Container class for the user name and the permission type: READ_ONLY,
* WRITE, or ADMIN.
*/
public class User implements Comparable<User>, Serializable {
public final static long serialVersionUID = 2L;
/**
* Name associated with anonymous user
*/
public static final String ANONYMOUS_USERNAME = "-anonymous-";
/**
* Value corresponding to Read-only permission for a repository user.
*/
public final static int READ_ONLY = 0;
/**
* Value corresponding to Write permission for a repository user.
*/
public final static int WRITE = 1;
/**
* Value corresponding to Administrative permission for a repository user.
*/
public final static int ADMIN = 2;
private final static String[] types = { "read-only", "write", "admin" };
private int permission;
private String name;
/**
* Constructor.
* @param name user id/name
* @param permission permission value (READ_ONLY, WRITE or ADMIN)
*/
public User(String name, int permission) {
this.name = name;
if (permission < READ_ONLY || permission > ADMIN) {
throw new IllegalArgumentException(
"Invalid type: " + permission + "; must be READ_ONLY, WRITE, or ADMIN");
}
this.permission = permission;
}
/**
* Returns user id/name
*/
public String getName() {
return name;
}
/**
* Returns true if permission is READ_ONLY.
*/
public boolean isReadOnly() {
return permission == READ_ONLY;
}
/**
* Return true if this user has permission of WRITE or ADMIN.
*/
public boolean hasWritePermission() {
return permission == WRITE || permission == ADMIN;
}
/**
* Returns true if permission is ADMIN.
*/
public boolean isAdmin() {
return permission == ADMIN;
}
/**
* Returns the permission value assigned this user.
*/
public int getPermissionType() {
return permission;
}
/*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append(name);
buf.append(" (");
buf.append(types[permission]);
buf.append(")");
return buf.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + permission;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (name == null) {
if (other.name != null)
return false;
}
else if (!name.equals(other.name))
return false;
if (permission != other.permission)
return false;
return true;
}
@Override
public int compareTo(User other) {
if (name == null) {
if (other.name != null)
return -1;
}
else if (other.name != null) {
return 1;
}
int rc = name.compareTo(other.name);
if (rc == 0) {
return permission - other.permission;
}
return rc;
}
}

View file

@ -0,0 +1,193 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.remote.security;
import ghidra.security.KeyStorePasswordProvider;
import java.io.*;
import ch.ethz.ssh2.crypto.Base64;
import ch.ethz.ssh2.crypto.PEMDecoder;
import ch.ethz.ssh2.signature.*;
public class SSHKeyManager {
// private static final String DEFAULT_KEYSTORE_PATH =
// System.getProperty("user.home") + File.separator + ".ssh/id_rsa";
//
// /**
// * Preference name for the SSH key file paths
// */
// private static final String SSH_KEYSTORE_PROPERTY = "ghidra.sshKeyFile";
// The public key file is derived by adding this extension to the key store filename
//private static final String SSH_PUBLIC_KEY_EXT1 = ".pub";
private static KeyStorePasswordProvider passwordProvider;
private SSHKeyManager() {
// static class--can't create
}
/**
* Set PKI protected keystore password provider
* @param provider
*/
public static synchronized void setProtectedKeyStorePasswordProvider(
KeyStorePasswordProvider provider) {
passwordProvider = provider;
}
// /**
// * Return the SSH private key for the current user. The default ~/.ssh/id_rsa file
// * will be used unless the System property <i>ghidra.sshKeyFile</i> has been set.
// * If the corresponding key file is encrypted the currently installed password
// * provider will be used to obtain the decrypt password.
// * @return RSAPrivateKey or DSAPrivateKey
// * @throws FileNotFoundException key file not found
// * @throws IOException if key file not found or key parse failed
// * @see RSAPrivateKey
// * @see DSAPrivateKey
// */
// public static Object getUsersSSHPrivateKey() throws IOException {
//
// String privateKeyStorePath = System.getProperty(SSH_KEYSTORE_PROPERTY);
// if (privateKeyStorePath == null) {
// privateKeyStorePath = DEFAULT_KEYSTORE_PATH;
// }
//
// return getSSHPrivateKey(new File(privateKeyStorePath));
// }
/**
* Return the SSH private key corresponding to the specified key file.
* If the specified key file is encrypted the currently installed password
* provider will be used to obtain the decrypt password.
* @param sshPrivateKeyFile
* @return RSAPrivateKey or DSAPrivateKey
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
* @see RSAPrivateKey
* @see DSAPrivateKey
*/
public static Object getSSHPrivateKey(File sshPrivateKeyFile) throws IOException {
if (!sshPrivateKeyFile.isFile()) {
throw new FileNotFoundException("SSH private key file not found: " + sshPrivateKeyFile);
}
InputStream keyIn = new FileInputStream(sshPrivateKeyFile);
try {
return getSSHPrivateKey(keyIn, sshPrivateKeyFile.getAbsolutePath());
}
finally {
try {
keyIn.close();
}
catch (IOException e) {
}
}
}
/**
* Return the SSH private key corresponding to the specified key input stream.
* If the specified key is encrypted the currently installed password
* provider will be used to obtain the decrypt password.
* @param sshPrivateKeyIn
* @return RSAPrivateKey or DSAPrivateKey
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
* @see RSAPrivateKey
* @see DSAPrivateKey
*/
public static Object getSSHPrivateKey(InputStream sshPrivateKeyIn) throws IOException {
return getSSHPrivateKey(sshPrivateKeyIn, "Protected SSH Key");
}
private static Object getSSHPrivateKey(InputStream sshPrivateKeyIn, String srcName)
throws IOException {
boolean isEncrypted = false;
StringBuffer keyBuf = new StringBuffer();
BufferedReader r = new BufferedReader(new InputStreamReader(sshPrivateKeyIn));
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("Proc-Type:")) {
isEncrypted = (line.indexOf("ENCRYPTED") > 0);
}
keyBuf.append(line);
keyBuf.append('\n');
}
r.close();
String password = null;
if (isEncrypted) {
char[] pwd = passwordProvider.getKeyStorePassword(srcName, false);
if (pwd == null) {
throw new IOException("Password required to open SSH private keystore");
}
// Don't like using String for password - but API doesn't give us a choice
password = new String(pwd);
}
return PEMDecoder.decode(keyBuf.toString().toCharArray(), password);
}
/**
* Attempt to instantiate an SSH public key from the specified file
* which contains a single public key.
* @param sshPublicKeyFile
* @return RSAPublicKey or DSAPublicKey
* @throws FileNotFoundException key file not found
* @throws IOException if key file not found or key parse failed
* @see RSAPublicKey
* @see DSAPublicKey
*/
public static Object getSSHPublicKey(File sshPublicKeyFile) throws IOException {
BufferedReader r = new BufferedReader(new FileReader(sshPublicKeyFile));
String keyLine = null;
String line;
while ((line = r.readLine()) != null) {
if (!line.startsWith("ssh-")) {
continue;
}
keyLine = line;
break;
}
r.close();
if (keyLine != null) {
String[] pieces = keyLine.split(" ");
if (pieces.length >= 2) {
byte[] pubkeyBytes = Base64.decode(pieces[1].toCharArray());
if ("ssh-rsa".equals(pieces[0])) {
return RSASHA1Verify.decodeSSHRSAPublicKey(pubkeyBytes);
}
else if ("ssh-dsa".equals(pieces[0])) {
return DSASHA1Verify.decodeSSHDSAPublicKey(pubkeyBytes);
}
}
}
throw new IOException(
"Invalid SSH public key file, valid ssh-rsa or ssh-dsa entry not found: " +
sshPublicKeyFile);
}
}

View file

@ -0,0 +1,76 @@
/* ###
* 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.store;
import ghidra.framework.remote.GhidraServerHandle;
/**
* <code>ChecoutType</code> identifies the type of checkout
*/
public enum CheckoutType {
/**
* Checkout is a normal non-exclusive checkout
*/
NORMAL,
/**
* Checkout is a persistent exclusive checkout which
* ensures no other checkout can occur while this checkout
* persists.
*/
EXCLUSIVE,
/**
* Similar to an EXCLUSIVE checkout, this checkout only
* persists while the associated client-connection is
* alive. This checkout is only permitted for remote
* versioned file systems which support its use.
*/
TRANSIENT;
/**
* Rely on standard Java serialization for enum
* If the above enum naming/order is changed, the server
* interface version must be changed
* @see GhidraServerHandle
*/
public static final long serialVersionUID = 1L;
/**
* Get the abbreviated/short name for this checkout type
* for use with serialization.
* @return short name
*/
public int getID() {
return name().charAt(0);
}
/**
* Get the CheckoutType whose name corresponds to the specified ID
* @param typeID checkout type ID
* @return CheckoutType of null if ID is invalid
*/
public static CheckoutType getCheckoutType(int typeID) {
for (CheckoutType type : values()) {
if (type.name().charAt(0) == typeID) {
return type;
}
}
return null;
}
}

View file

@ -0,0 +1,163 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import java.io.IOException;
/**
* <code>DataFileHandle</code> provides a random-access handle to a file.
*/
public interface DataFileHandle {
/**
* Returns true if this data file handle is open read-only.
* @throws IOException if an I/O error occurs.
*/
public boolean isReadOnly() throws IOException;
/**
* Reads <code>b.length</code> bytes from this file into the byte
* array, starting at the current file pointer. This method reads
* repeatedly from the file until the requested number of bytes are
* read. This method blocks until the requested number of bytes are
* read, the end of the stream is detected, or an exception is thrown.
*
* @param b the buffer into which the data is read.
* @exception EOFException if this file reaches the end before reading
* all the bytes.
* @exception IOException if an I/O error occurs.
*/
public void read(byte b[]) throws IOException;
/**
* Reads exactly <code>len</code> bytes from this file into the byte
* array, starting at the current file pointer. This method reads
* repeatedly from the file until the requested number of bytes are
* read. This method blocks until the requested number of bytes are
* read, the end of the stream is detected, or an exception is thrown.
*
* @param b the buffer into which the data is read.
* @param off the start offset of the data.
* @param len the number of bytes to read.
* @exception EOFException if this file reaches the end before reading
* all the bytes.
* @exception IOException if an I/O error occurs.
*/
public void read(byte b[], int off, int len) throws IOException;
/**
* Attempts to skip over <code>n</code> bytes of input discarding the
* skipped bytes.
* <p>
*
* This method may skip over some smaller number of bytes, possibly zero.
* This may result from any of a number of conditions; reaching end of
* file before <code>n</code> bytes have been skipped is only one
* possibility. This method never throws an <code>EOFException</code>.
* The actual number of bytes skipped is returned. If <code>n</code>
* is negative, no bytes are skipped.
*
* @param n the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @exception IOException if an I/O error occurs.
*/
public int skipBytes(int n) throws IOException;
/**
* Writes the specified byte to this file. The write starts at
* the current file pointer.
*
* @param b the <code>byte</code> to be written.
* @exception IOException if an I/O error occurs.
*/
public void write(int b) throws IOException;
/**
* Writes <code>b.length</code> bytes from the specified byte array
* to this file, starting at the current file pointer.
*
* @param b the data.
* @exception IOException if an I/O error occurs.
*/
public void write(byte b[]) throws IOException;
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this file.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @exception IOException if an I/O error occurs.
*/
public void write(byte b[], int off, int len) throws IOException;
/**
* Sets the file-pointer offset, measured from the beginning of this
* file, at which the next read or write occurs. The offset may be
* set beyond the end of the file. Setting the offset beyond the end
* of the file does not change the file length. The file length will
* change only by writing after the offset has been set beyond the end
* of the file.
*
* @param pos the offset position, measured in bytes from the
* beginning of the file, at which to set the file
* pointer.
* @exception IOException if <code>pos</code> is less than
* <code>0</code> or if an I/O error occurs.
*/
public void seek(long pos) throws IOException;
/**
* Returns the length of this file.
*
* @return the length of this file, measured in bytes.
* @exception IOException if an I/O error occurs.
*/
public long length() throws IOException;
/**
* Sets the length of this file.
*
* <p> If the present length of the file as returned by the
* <code>length</code> method is greater than the <code>newLength</code>
* argument then the file will be truncated. In this case, if the file
* offset as returned by the <code>getFilePointer</code> method is greater
* then <code>newLength</code> then after this method returns the offset
* will be equal to <code>newLength</code>.
*
* <p> If the present length of the file as returned by the
* <code>length</code> method is smaller than the <code>newLength</code>
* argument then the file will be extended. In this case, the contents of
* the extended portion of the file are not defined.
*
* @param newLength The desired length of the file
* @exception IOException If an I/O error occurs
*/
public void setLength(long newLength) throws IOException;
/**
* Closes this random access file stream and releases any system
* resources associated with the stream. A closed random access
* file cannot perform input or output operations and cannot be
* reopened.
*
* @exception IOException if an I/O error occurs.
*/
public void close() throws IOException;
}

View file

@ -0,0 +1,54 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import java.io.*;
import java.io.InputStream;
import java.io.OutputStream;
/**
* <code>DataFileItem</code> corresponds to a private serialized
* data file within a FileSystem. Methods are provided for opening
* the underlying file as an input or output stream.
* <br>
* NOTE: The use of DataFile is not encouraged and is not fully
* supported.
*/
public interface DataFileItem extends FolderItem {
/**
* Open the current version of this item for reading.
* @return input stream
* @throws FileNotFoundException
*/
InputStream getInputStream() throws FileNotFoundException;
/**
* Open a new version of this item for writing.
* @return output stream.
* @throws FileNotFoundException
*/
OutputStream getOutputStream() throws FileNotFoundException;
/**
* Open a specific version of this item for reading.
* @return input stream
* @throws FileNotFoundException
*/
InputStream getInputStream(int version) throws FileNotFoundException;
}

View file

@ -0,0 +1,80 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import ghidra.util.exception.FileInUseException;
import java.io.IOException;
import db.buffers.ManagedBufferFile;
/**
* <code>DatabaseItem</code> corresponds to a private or versioned
* database within a FileSystem. Methods are provided for opening
* the underlying database as a BufferFile.
*/
public interface DatabaseItem extends FolderItem {
/**
* Open a specific version of the stored database for non-update use.
* Historical change data from minChangeDataVer through version is available.
* The returned BufferFile does not support the BufferMgr's Save operation.
* @param version database version
* @param minChangeDataVer indicates the oldest change data version to be
* included in change set. A -1 indicates only the last change data buffer file is applicable.
* @return buffer file
* @throws FileInUseException thrown if unable to obtain the required database lock(s).
* @throws IOException thrown if IO error occurs.
* @see ManagedBufferFile#getNextChangeDataFile(boolean)
*/
ManagedBufferFile open(int version, int minChangeDataVer) throws IOException;
/**
* Open a specific version of the stored database for non-update use.
* Change data will not be available.
* The returned BufferFile does not support the BufferMgr's Save operation.
* @param version database version
* @return buffer file
* @throws FileInUseException thrown if unable to obtain the required database lock(s).
* @throws IOException thrown if IO error occurs.
*/
ManagedBufferFile open(int version) throws IOException;
/**
* Open the current version of the stored database for non-update use.
* Change data will not be available.
* The returned BufferFile does not support the BufferMgr's Save operation.
* @throws IOException thrown if IO error occurs.
*/
ManagedBufferFile open() throws IOException;
/**
* Open the current version of the stored database for update use.
* The returned BufferFile supports the Save operation.
* If this item is on a shared file-system, this method initiates an
* item checkin. If a changeSet is specified, it will be filled with
* all change data since the check-out version. Change data will be
* read into the change set starting oldest to newest.
* @param checkoutId the associated checkoutId if this item is stored
* on a versioned file-system, otherwise DEFAULT_CHECKOUT_ID can be
* specified.
* @return buffer file
* @throws FileInUseException thrown if unable to obtain the required database lock(s).
* @throws IOException thrown if IO error occurs.
*/
ManagedBufferFile openForUpdate(long checkoutId) throws IOException;
}

View file

@ -0,0 +1,25 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import java.io.IOException;
public class ExclusiveCheckoutException extends IOException {
public ExclusiveCheckoutException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,70 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import javax.net.ServerSocketFactory;
/**
* Factory class for generating unique file ID's.
*/
public class FileIDFactory {
private FileIDFactory() {
}
public static String createFileID() {
try {
// Ensure time uniqueness within process
Thread.sleep(2);
} catch (InterruptedException e1) {
}
int uniquePort = 0;
byte[] addrBytes = null;
ServerSocket serverSocket = null;
try {
StringBuffer buf = new StringBuffer();
serverSocket = ServerSocketFactory.getDefault().createServerSocket();
serverSocket.bind(null);
uniquePort = serverSocket.getLocalPort();
addrBytes = InetAddress.getLocalHost().getAddress();
for (int i = 0; i < 4; i++) {
int b = addrBytes[i] & 0xff;
buf.append(Integer.toHexString(b));
}
buf.append(Integer.toHexString(uniquePort));
buf.append(System.nanoTime());
return buf.toString();
} catch (IOException e) {
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
}
}
}
// We do not have a network interface :(
// TODO: this case could use improvement - possible exposure is use of shared project in an off-line mode
return Long.toHexString(System.nanoTime());
}
}

View file

@ -0,0 +1,293 @@
/* ###
* 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.store;
import java.io.*;
import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
* <code>FileSystem</code> provides a hierarchical view and management of a
* set of files and folders.
*/
public interface FileSystem {
/**
* Character used to separate folder and item names within a path string.
*/
public static final char SEPARATOR_CHAR = '/';
public static final String SEPARATOR = Character.toString(SEPARATOR_CHAR);
/**
* Get user name associated with this filesystem. In the case of a remote filesystem
* this will correspond to the name used during login/authentication. A null value may
* be returned if user name unknown.
*/
String getUserName();
/**
* Returns true if the file-system requires check-outs when
* modifying folder items.
*/
public boolean isVersioned();
/**
* Returns true if file-system is on-line.
*/
public boolean isOnline();
/**
* Returns true if file-system is read-only.
* @throws IOException
*/
public boolean isReadOnly() throws IOException;
/**
* Returns the number of folder items contained within this file-system.
* @throws IOException
* @throws UnsupportedOperationException if file-system does not support this operation
*/
public int getItemCount() throws IOException, UnsupportedOperationException;
/**
* Returns a list of the folder item names contained in the given folder.
* @param folderPath the path of the folder.
* @return a list of folder item names.
* @throws IOException
*/
public String[] getItemNames(String folderPath) throws IOException;
/**
* Returns the FolderItem in the given folder with the given name
* @param folderPath the folder path containing the item.
* @param name the name of the item.
* @return the FolderItem with the given folderPath and name, or null if it doesn't exist.
* @throws IOException if IO error occurs.
*/
public FolderItem getItem(String folderPath, String name) throws IOException;
/**
* Returns the FolderItem specified by its unique File-ID
* @param fileID the items unique file ID
* @return the FolderItem with the given folderPath and name, or null if it doesn't exist.
* @throws IOException if IO error occurs.
* @throws UnsupportedOperationException if file-system does not support this operation
*/
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException;
/**
* Return a list of subfolders (by name) that are stored within the specified folder path.
* @throws FileNotFoundException if folder path does not exist.
* @throws IOException if IO error occurs.
*/
public String[] getFolderNames(String folderPath) throws IOException;
/**
* Creates a new subfolder within the specified parent folder.
* @param parentPath folder path of parent
* @param folderName name of new subfolder
* @throws DuplicateFileException if a folder exists with this name
* @throws InvalidNameException if the name does not have
* all alphanumerics
* @throws IOException thrown if an IO error occurs.
*/
public void createFolder(String parentPath, String folderName) throws InvalidNameException,
IOException;
/**
* Create a new database item within the specified parent folder using the contents
* of the specified BufferFile.
* @param parentPath folder path of parent
* @param name new database name
* @param fileID file ID to be associated with new database or null
* @param bufferFile data source
* @param comment version comment (used for versioned file system only)
* @param contentType application defined content type
* @param resetDatabaseId if true database ID will be reset for new Database
* @param monitor allows the database copy to be monitored and cancelled.
* @param user name of user creating item (required for versioned item)
* @return new DatabaseItem
* @throws FileNotFoundException thrown if parent folder does not exist.
* @throws DuplicateFileException if a folder item exists with this name
* @throws InvalidNameException if the name does not have
* all alphanumerics
* @throws IOException if an IO error occurs.
* @throws CancelledException if cancelled by monitor
*/
public DatabaseItem createDatabase(String parentPath, String name, String fileID,
BufferFile bufferFile, String comment, String contentType, boolean resetDatabaseId,
TaskMonitor monitor, String user) throws InvalidNameException, IOException,
CancelledException;
/**
* Create a new empty database item within the specified parent folder.
* If this is a versioned file-system, the associated item is checked-out.
* The resulting checkoutId can be obtained from the returned buffer file.
* @param parentPath folder path of parent
* @param name new database name
* @param fileID file ID to be associated with new database or null
* @param bufferFile data source
* @param contentType application defined content type
* @param bufferSize buffer size. If copying an existing BufferFile, the buffer
* size must be the same as the source file.
* @param user name of user creating item (required for versioned item)
* @param projectPath path of project in which database is checked-out (required for versioned item)
* @return an empty BufferFile open for read-write.
* @throws FileNotFoundException thrown if parent folder does not exist.
* @throws DuplicateFileException if a folder item exists with this name
* @throws InvalidNameException if the name does not have
* all alphanumerics
* @throws IOException if an IO error occurs.
*/
public ManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath)
throws InvalidNameException, IOException;
/**
* Creates a new empty data file within the specified parent folder.
* @param parentPath folder path of parent
* @param name new data file name
* @param inputStream source data
* @param comment version comment (used for versioned file system only)
* @param contentType application defined content type
* @param monitor progress monitor (used for cancel support,
* progress not used since length of input stream is unknown)
* @return new data file
* @throws DuplicateFileException Thrown if a folderItem with that name already exists.
* @throws InvalidNameException if the name has illegal characters.
* all alphanumerics
* @throws IOException if an IO error occurs.
* @throws CancelledException if cancelled by monitor
*/
public DataFileItem createDataFile(String parentPath, String name, InputStream istream,
String comment, String contentType, TaskMonitor monitor) throws InvalidNameException,
IOException, CancelledException;
/**
* Creates a new file item from a packed file.
* The content/item type must be determined from the input stream.
* @param parentPath folder path of parent
* @param name new data file name
* @param packedFile packed file data
* @param monitor progress monitor (used for cancel support,
* progress not used since length of input stream is unknown)
* @param user name of user creating item (required for versioned item)
* @return new item
* @throws InvalidNameException if the name has illegal characters.
* all alphanumerics
* @throws IOException if an IO error occurs.
* @throws CancelledException if cancelled by monitor
*/
public FolderItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user) throws InvalidNameException, IOException,
CancelledException;
/**
* Delete the specified folder.
* @param folderPath path of folder to be deleted
* @throws FolderNotEmptyException Thrown if the folder is not empty.
* @throws FileNotFoundException if there is no folder with the given path name.
* @throws IOException if error occured during delete.
*/
public void deleteFolder(String folderPath) throws IOException;
/**
* Move the specified folder to the path specified by newFolderPath.
* The moved folder must not be an ancestor of the new Parent.
* @param parentPath path of parent folder that the moving folder currently resides in.
* @param folderName name of the folder within the parentPath to be moved.
* @param newParentPath path to where the folder is to be moved.
* @throws FileNotFoundException if the moved folder does not exist.
* @throws DuplicateFileException if folder with the same name exists within the new parent folder
* @throws FileInUseException if any file within this folder or its decendents are in-use or checked-out
* @throws IOException if an IO error occurs.
* @throws InvalidNameException if the new FolderPath contains an illegal file name.
* @throws IllegalArgumentException if new Parent is invalid.
*/
public void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException;
/**
* Renames the specified folder to a new name.
* @param parentPath the parent folder of the folder to be renamed.
* @param folderName the current name of the folder to be renamed.
* @param newFolderName the name the folder to be renamed to.
* @throws FileNotFoundException if the folder to be renamed does not exist.
* @throws DuplicateFileException if folder with the new name already exists.
* @throws FileInUseException if any file within this folder or its decendents are in-use or checked-out
* @throws IOException if an IO error occurs.
* @throws InvalidNameException if the new FolderName contains an illegal file name.
*/
public void renameFolder(String parentPath, String folderName, String newFolderName)
throws InvalidNameException, IOException;
/**
* Moves the specified item to a new folder.
* @param folderPath path of folder containing the item.
* @param name name of the item to be moved.
* @param newFolderPath path of folder where item is to be moved.
* @throws FileNotFoundException if the item does not exist.
* @throws DuplicateFileException if item with the same name exists within the new parent folder.
* @throws FileInUseException if the item is in-use or checked-out
* @throws IOException if an IO error occurs.
* @throws InvalidNameException if the newName is invalid
*/
public void moveItem(String folderPath, String name, String newFolderPath, String newName)
throws IOException, InvalidNameException;
/**
* Adds the given listener to be notified of file system changes.
* @param listener the listner to be added.
*/
public void addFileSystemListener(FileSystemListener listener);
/**
* Removes the listener from being notified of file system changes.
* @param listener
*/
public void removeFileSystemListener(FileSystemListener listener);
/**
* Returns true if the folder specified by the path exists.
* @param folderPath the name of the folder to check for existence.
* @return true if the folder exists.
* @throws IOException if an IO error occurs.
*/
public boolean folderExists(String folderPath) throws IOException;
/**
* Returns true if the file exists
* @param folderPath the folderPath of the folder that may contain the file.
* @param name the name of the file to check for existence.
* @throws IOException if an IO error occurs.
*/
public boolean fileExists(String folderPath, String name) throws IOException;
/**
* Returns true if this file system is shared
*/
public boolean isShared();
/**
* Cleanup & release resources
*/
public void dispose();
}

View file

@ -0,0 +1,32 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import ghidra.framework.ModuleInitializer;
import ghidra.framework.store.db.PackedDatabase;
public class FileSystemInitializer implements ModuleInitializer {
public void run() {
PackedDatabase.cleanupOldTempDatabases();
}
@Override
public String getName() {
return "FileSystem Module";
}
}

View file

@ -0,0 +1,98 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
/**
* <code>FileSystemListener</code> provides a listener the ability
* to be notified of folder and file changes within a FileSystem.
*/
public interface FileSystemListener {
/**
* Notification that a new folder was created.
* @param parentPath the path of the folder that contains the new folder
* @param name the name of the new folder
*/
void folderCreated(String parentPath, String name);
/**
* Notification that a new folder item was created.
* @param parentPath the path of the folder that contains the new item.
* @param name the name of the new item.
*/
void itemCreated(String parentPath, String name);
/**
* Notification that a folder was deleted.
* @param parentPath the path of the folder that contained the deleted folder.
* @param folderName the name of the folder that was deleted.
*/
void folderDeleted(String parentPath, String folderName);
/**
* Notification that a folder was moved.
* @param parentPath the path of the folder that used to contain the moved folder.
* @param folderName the name of the folder that was moved.
* @param newParentPath the path of the folder that now contains the moved folder.
*/
void folderMoved(String parentPath, String folderName, String newParentPath);
/**
* Notification that a folder was renamed.
* @param parentPath the path of the folder containing the folder that was renamed.
* @param oldFolderName the old name of the folder.
* @param newFolderName the new name of the folder.
*/
void folderRenamed(String parentPath, String oldFolderName, String newFolderName);
/**
* Notification that a folder item was deleted.
* @param folderPath the path of the folder that contained the deleted item.
* @param itemName the name of the item that was deleted.
*/
void itemDeleted(String folderPath, String itemName);
/**
* Notification that an item was renamed.
* @param folderPath the path of the folder that contains the renamed item
* @param oldItemName the old name of the item.
* @param newITemName the new name of the item.
*/
void itemRenamed(String folderPath, String oldItemName, String newItemName);
/**
* Notification that an item was moved.
* @param parentPath the path of the folder that used to contain the item.
* @param itemName the name of the item that was moved.
* @param newParentPath the path of the folder that the item was moved to.
* @param newName the new name of the item.
*/
void itemMoved(String parentPath, String name, String newParentPath, String newName);
/**
* Notfication that an item's state has changed.
* @param parentPath the path of the folder containing the item.
* @param itemName the name of the item that has changed.
*/
void itemChanged(String parentPath, String itemName);
/**
* Perform a full refresh / synchronization
*/
void syncronize();
}

View file

@ -0,0 +1,438 @@
/* ###
* 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.store;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* <code>FileSystemListenerList</code> maintains a list of FileSystemListener's.
* This class, acting as a FileSystemListener, simply relays each callback to
* all FileSystemListener's within its list. Employs either a synchronous
* and asynchronous notification mechanism.
*/
public class FileSystemListenerList implements FileSystemListener {
private List<FileSystemListener> listenerList = new CopyOnWriteArrayList<>();
private List<FileSystemEvent> events =
Collections.synchronizedList(new LinkedList<FileSystemEvent>());
private boolean enableAsynchronousDispatching;
private boolean isEventProcessingThreadWaiting;
private boolean alive = true;
private Object lock = new Object();
private Thread thread;
/**
* Construct FileSystemListenerList
* @param enableAsyncronousDispatching if true a seperate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
*/
public FileSystemListenerList(boolean enableAsynchronousDispatching) {
this.enableAsynchronousDispatching = enableAsynchronousDispatching;
}
public void dispose() {
alive = false;
synchronized (lock) {
lock.notify();
}
}
/**
* Add a listener to this list.
* @param listener
*/
public synchronized void add(FileSystemListener listener) {
listenerList.add(listener);
if (thread == null && enableAsynchronousDispatching) {
thread = new FileSystemEventProcessingThread();
thread.setName("File System Listener");
thread.start();
}
}
/**
* Remove a listener from this list.
* @param listener
*/
public void remove(FileSystemListener listener) {
listenerList.remove(listener);
}
/**
* Remove all listeners from this list.
*/
public void clear() {
listenerList.clear();
}
/**
* Forwards itemMoved callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#itemMoved(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void itemMoved(String parentPath, String name, String newParentPath, String newName) {
if (enableAsynchronousDispatching) {
add(new ItemMovedEvent(parentPath, name, newParentPath, newName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemMoved(parentPath, name, newParentPath, newName);
}
}
}
/**
* Forwards itemRenamed callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#itemRenamed(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void itemRenamed(String parentPath, String itemName, String newName) {
if (enableAsynchronousDispatching) {
add(new ItemRenamedEvent(parentPath, itemName, newName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemRenamed(parentPath, itemName, newName);
}
}
}
/**
* Forwards itemDeleted callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#itemDeleted(java.lang.String, java.lang.String)
*/
@Override
public void itemDeleted(String parentPath, String itemName) {
if (enableAsynchronousDispatching) {
add(new ItemDeletedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemDeleted(parentPath, itemName);
}
}
}
/**
* Forwards folderRenamed callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#folderRenamed(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void folderRenamed(String parentPath, String folderName, String newFolderName) {
if (enableAsynchronousDispatching) {
add(new FolderRenamedEvent(parentPath, folderName, newFolderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderRenamed(parentPath, folderName, newFolderName);
}
}
}
/**
* Forwards folderMoved callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#folderMoved(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void folderMoved(String parentPath, String folderName, String newParentPath) {
if (enableAsynchronousDispatching) {
add(new FolderMovedEvent(parentPath, folderName, newParentPath));
}
else {
for (FileSystemListener l : listenerList) {
l.folderMoved(parentPath, folderName, newParentPath);
}
}
}
/**
* Forwards folderDeleted callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#folderDeleted(java.lang.String, java.lang.String)
*/
@Override
public void folderDeleted(String parentPath, String folderName) {
if (enableAsynchronousDispatching) {
add(new FolderDeletedEvent(parentPath, folderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderDeleted(parentPath, folderName);
}
}
}
/**
* Forwards itemCreated callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#itemCreated(java.lang.String, java.lang.String)
*/
@Override
public void itemCreated(String parentPath, String itemName) {
if (enableAsynchronousDispatching) {
add(new ItemCreatedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemCreated(parentPath, itemName);
}
}
}
/**
* Forwards folderCreated callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#folderCreated(java.lang.String, java.lang.String)
*/
@Override
public void folderCreated(String parentPath, String folderName) {
if (enableAsynchronousDispatching) {
add(new FolderCreatedEvent(parentPath, folderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderCreated(parentPath, folderName);
}
}
}
/**
* Forwards itemChanged callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#itemChanged(java.lang.String, java.lang.String)
*/
@Override
public void itemChanged(String parentPath, String itemName) {
if (enableAsynchronousDispatching) {
add(new ItemChangedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemChanged(parentPath, itemName);
}
}
}
/**
* Forwards syncronize callback to all listeners within this list.
* @see ghidra.framework.store.FileSystemListener#syncronize()
*/
@Override
public void syncronize() {
if (enableAsynchronousDispatching) {
add(new SynchronizeEvent());
}
}
private void add(FileSystemEvent ev) {
if (!listenerList.isEmpty()) {
events.add(ev);
synchronized (lock) {
lock.notify();
}
}
}
/**
* Returns true if this class is processing events <b>or</b> needs to process events that are
* in its event queue.
*
* @return true if this class is processing events <b>or</b> needs to process events that are
* in its event queue.
*/
public boolean isProcessingEvents() {
synchronized (this) {
if (thread == null) {
return false; // non-threaded; does not 'process' events, done synchronously
}
}
synchronized (lock) { // lock so nobody adds new events
return !isEventProcessingThreadWaiting || (events.size() > 0);
}
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class FileSystemEventProcessingThread extends Thread {
FileSystemEventProcessingThread() {
super("File System Event Processor");
setDaemon(true);
}
@Override
public void run() {
while (alive) {
while (!events.isEmpty()) {
FileSystemEvent event;
synchronized (lock) {
event = events.remove(0);
}
synchronized (FileSystemListenerList.this) {
for (FileSystemListener l : listenerList) {
event.dispatch(l);
}
}
}
doWait();
}
}
private void doWait() {
try {
synchronized (lock) {
if (alive && events.isEmpty()) {
isEventProcessingThreadWaiting = true;
lock.wait();
}
}
}
catch (InterruptedException e) {
}
finally {
isEventProcessingThreadWaiting = false;
}
}
}
private static abstract class FileSystemEvent {
String parentPath;
String name;
String newParentPath;
String newName;
FileSystemEvent(String parentPath, String name, String newParentPath, String newName) {
this.parentPath = parentPath;
this.name = name;
this.newParentPath = newParentPath;
this.newName = newName;
}
abstract void dispatch(FileSystemListener listener);
}
private static class ItemMovedEvent extends FileSystemEvent {
ItemMovedEvent(String parentPath, String name, String newParentPath, String newName) {
super(parentPath, name, newParentPath, newName);
}
@Override
void dispatch(FileSystemListener listener) {
listener.itemMoved(parentPath, name, newParentPath, newName);
}
}
private static class ItemRenamedEvent extends FileSystemEvent {
ItemRenamedEvent(String parentPath, String name, String newName) {
super(parentPath, name, null, newName);
}
@Override
void dispatch(FileSystemListener listener) {
listener.itemRenamed(parentPath, name, newName);
}
}
private static class ItemDeletedEvent extends FileSystemEvent {
ItemDeletedEvent(String parentPath, String name) {
super(parentPath, name, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.itemDeleted(parentPath, name);
}
}
private static class FolderMovedEvent extends FileSystemEvent {
FolderMovedEvent(String parentPath, String name, String newParentPath) {
super(parentPath, name, newParentPath, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.folderMoved(parentPath, name, newParentPath);
}
}
private static class FolderRenamedEvent extends FileSystemEvent {
FolderRenamedEvent(String parentPath, String name, String newName) {
super(parentPath, name, null, newName);
}
@Override
void dispatch(FileSystemListener listener) {
listener.folderRenamed(parentPath, name, newName);
}
}
private static class FolderDeletedEvent extends FileSystemEvent {
FolderDeletedEvent(String parentPath, String name) {
super(parentPath, name, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.folderDeleted(parentPath, name);
}
}
private static class ItemCreatedEvent extends FileSystemEvent {
ItemCreatedEvent(String parentPath, String name) {
super(parentPath, name, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.itemCreated(parentPath, name);
}
}
private static class FolderCreatedEvent extends FileSystemEvent {
FolderCreatedEvent(String parentPath, String name) {
super(parentPath, name, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.folderCreated(parentPath, name);
}
}
private static class ItemChangedEvent extends FileSystemEvent {
ItemChangedEvent(String parentPath, String name) {
super(parentPath, name, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.itemChanged(parentPath, name);
}
}
private static class SynchronizeEvent extends FileSystemEvent {
SynchronizeEvent() {
super(null, null, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
listener.syncronize();
}
}
}

View file

@ -0,0 +1,301 @@
/* ###
* 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.store;
import java.io.File;
import java.io.IOException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>FolderItem</code> represents an individual file
* contained within a FileSystem and is uniquely identified
* by a path string.
*/
public interface FolderItem {
/**
* Underlying file is an unknown/unsupported type.
*/
public static final int UNKNOWN_FILE_TYPE = -1;
/**
* Underlying file is a Database
*/
public static final int DATABASE_FILE_TYPE = 0;
/**
* Underlying file is serialized data file
*/
public static final int DATAFILE_FILE_TYPE = 1;
/**
* Default checkout ID used when a checkout is not applicable.
*/
public static final long DEFAULT_CHECKOUT_ID = -1;
/**
* Default file version number used to indicate the latest/current version.
*/
public static final int LATEST_VERSION = -1;
/**
* Return The display name for this item.
*/
String getName();
/**
* Return the file ID if one has been established or null
* @throws IOException thrown if IO or access error occurs
*/
String getFileID() throws IOException;
/**
* Assign a new file-ID to this local non-versioned file.
* NOTE: This method is only valid for a local non-versioned file-system.
* @return new file-ID
* @throws IOException thrown if IO or access error occurs
*/
String resetFileID() throws IOException;
/**
* Returns the length of this domain file. This size is the minimum disk space
* used for storing this file, but does not account for additional storage space
* used to tracks changes, etc.
* @return file length
* @throws IOException thrown if IO or access error occurs
*/
long length() throws IOException;
/**
* Return The content type name for this item.
*/
String getContentType();
/**
* Returns the path of the parent folder.
*/
String getParentPath();
/**
* Return The concatenation of the pathname and the basename
* which can be used to uniquely identify a folder item.
*/
String getPathName();
/**
* Returns true if item can be overwritten/deleted.
*/
boolean isReadOnly();
/**
* Set the state of the read-only indicator for this non-shared item.
* @param state read-only state
* @throws IOException if an IO error occurs or item is
* stored on a shared file-system
*/
void setReadOnly(boolean state) throws IOException;
/**
* Returns the version of content type. Note this is the version of the structure/storage
* for the content type, Not the users version of their data.
*/
int getContentTypeVersion();
/**
* Sets the version for the content type. This will change whenever the domain objects
* are upgraded.
* @param version the new version for the content type.
* @throws IOException if an IO error occurs or item is
* stored on a shared file-system
*/
void setContentTypeVersion(int version) throws IOException;
/**
* Return The time that this item was last modified.
*/
long lastModified();
/**
* Return the latest/current version.
*/
int getCurrentVersion();
/**
* Returns true if this item is a checked-out copy from a versioned file system.
*/
boolean isCheckedOut();
/**
* Returns true if this item is a checked-out copy with exclusive access from a versioned file system.
*/
boolean isCheckedOutExclusive();
/**
* Return true if this is a versioned item, else false
* @throws IOException thrown if an IO error occurs.
*/
boolean isVersioned() throws IOException;
/**
* Returns the checkoutId for this file. A value of -1 indicates
* a private item.
* NOTE: This method is only valid for a local non-versioned file-system.
* @throws IOException if an IO error occurs
*/
long getCheckoutId() throws IOException;
/**
* Returns the item version which was checked-out. A value of -1 indicates
* a private item.
* NOTE: This method is only valid for a local non-versioned file-system.
* @throws IOException
*/
int getCheckoutVersion() throws IOException;
/**
* Returns the local item version at the time the checkout was
* completed. A value of -1 indicates a private item.
* NOTE: This method is only valid for a local non-versioned file-system.
*/
int getLocalCheckoutVersion();
/**
* Set the checkout data associated with this non-shared file.
* NOTE: This method is only valid for a local non-versioned file-system.
* @param checkoutId checkout ID (provided by ItemCheckoutStatus).
* @param exclusive true if checkout is exclusive
* @param checkoutVersion the item version which was checked-out (provided
* by ItemCheckoutStatus).
* @param localVersion the local item version at the time the checkout was
* completed.
* @throws IOException if an IO error occurs or item is
* stored on a shared file-system
*/
void setCheckout(long checkoutId, boolean exclusive, int checkoutVersion, int localVersion)
throws IOException;
/**
* Clears the checkout data associated with this non-shared file.
* NOTE: This method is only valid for a local non-versioned file-system.
* @throws IOException
*/
void clearCheckout() throws IOException;
/**
* Deletes the item or a specific version. If a specific version
* is specified, it must either be the oldest or latest (i.e., current).
* @param version specific version to be deleted, or -1 to remove
* all versions.
* @param user user name
* @throws IOException if an IO error occurs, including the inability
* to delete a version because this item is checked-out, the user does
* not have permission, or the specified version is not the oldest or
* latest.
*/
void delete(int version, String user) throws IOException;
/**
* Returns list of all available versions or null
* if item is not versioned.
* @throws IOException thrown if an IO error occurs.
*/
Version[] getVersions() throws IOException;
/**
* Checkout this folder item.
* @param checkoutType type of checkout
* @param user user requesting checkout
* @param projectPath path of project where checkout was made
* @return checkout status or null if exclusive checkout request failed
* @throws IOException if an IO error occurs or this item is not versioned
*/
ItemCheckoutStatus checkout(CheckoutType checkoutType, String user, String projectPath)
throws IOException;
/**
* Terminates a checkout. The checkout ID becomes invalid, therefore the
* associated checkout copy should either be removed or converted to a
* private file.
* @param checkoutId checkout ID
* @param notify if true item change notification will be sent
* @throws IOException if an IO error occurs or this item is not versioned
*/
void terminateCheckout(long checkoutId, boolean notify) throws IOException;
/**
* Returns true if this item is versioned and has one or more checkouts.
* @throws IOException if an IO error occurs
*/
boolean hasCheckouts() throws IOException;
/**
* Returns true if unsaved file changes can be recovered.
*/
boolean canRecover();
/**
* Get the checkout status which corresponds to the specified checkout ID.
* @param checkoutId checkout ID
* @return checkout status or null if checkout ID not found.
* @throws IOException if an IO error occurs or this item is not versioned
*/
ItemCheckoutStatus getCheckout(long checkoutId) throws IOException;
/**
* Get all current checkouts for this item.
* @param parentPath
* @param itemName
* @return list of checkouts
* @throws IOException if an IO error occurs or this item is not versioned
*/
ItemCheckoutStatus[] getCheckouts() throws IOException;
/**
* Returns true if this item is versioned and has a checkin in-progress.
* @throws IOException if an IO error occurs
*/
boolean isCheckinActive() throws IOException;
/**
* Update the checkout version associated with this versioned item.
* @param checkoutId id corresponding to an existing checkout
* @param checkoutVersion
* @param user
* @throws IOException if an IO error occurs.
*/
void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
throws IOException;
/**
* Serialize (i.e., pack) this item into the specified outputFile.
* @param outputFile packed output file to be created
* @param version if this item is versioned, specifies the version to be output, otherwise
* -1 should be specified.
* @param monitor progress monitor
* @throws IOException
* @throws CancelledException if monitor cancels operation
*/
public void output(File outputFile, int version, TaskMonitor monitor) throws IOException,
CancelledException;
/**
* Returns this instance after refresh or null if item no longer exists
*/
public FolderItem refresh() throws IOException;
}

View file

@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import java.io.IOException;
/**
* <code>FolderNotEmptyException</code> is thrown when an attempt is
* made to remove a Folder which is not empty.
*/
public class FolderNotEmptyException extends IOException {
/**
* Constructor.
* @param msg error message
*/
public FolderNotEmptyException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,265 @@
/* ###
* 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.store;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
/**
* <code>ItemCheckoutStatus</code> provides immutable status information for a
* checked-out item. This class is serializable so that it may be passed
* to a remote client.
*/
public class ItemCheckoutStatus implements java.io.Serializable {
public static final long serialVersionUID = 1L;
private static final int VERSION = 3;
private long checkoutId;
private String user;
private int version;
private long time;
private String projectPath;
private CheckoutType checkoutType;
/**
* Constructor.
* @param checkoutId unique checkout ID
* @param checkoutType type of checkout
* @param user user name
* @param version version of file which was checked-out
* @param time time when checkout was completed.
*/
public ItemCheckoutStatus(long checkoutId, CheckoutType checkoutType, String user, int version,
long time, String projectPath) {
this.checkoutId = checkoutId;
this.checkoutType = checkoutType;
this.user = user;
this.version = version;
this.time = time;
if (projectPath != null) {
projectPath = projectPath.replace('\\', '/');
}
this.projectPath = projectPath;
}
/**
* Serialization method
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.writeInt(VERSION);
out.writeLong(checkoutId);
out.writeUTF(user);
out.writeInt(version);
out.writeLong(time);
out.writeInt(checkoutType.getID());
out.writeUTF(projectPath != null ? projectPath : "");
}
/**
* Deserialization method
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
long ver = in.readInt();
if (ver > VERSION) {
throw new ClassNotFoundException("Unsupported version of ItemCheckoutStatus");
}
checkoutId = in.readLong();
user = in.readUTF();
version = in.readInt();
time = in.readLong();
if (ver < 3) {
checkoutType = in.readBoolean() ? CheckoutType.EXCLUSIVE : CheckoutType.NORMAL;
}
else { // Transient checkout added with Version 3
int checkoutTypeId = in.readInt();
checkoutType = CheckoutType.getCheckoutType(checkoutTypeId);
if (checkoutType == null) {
throw new IOException("Invalid ItemCheckoutStatus Type: " + checkoutTypeId);
}
}
if (ver > 1) { // Client project path added with Version 2
projectPath = in.readUTF();
if (projectPath.length() == 0) {
projectPath = null;
}
}
}
/**
* Returns the unique ID for the associated checkout.
*/
public long getCheckoutId() {
return checkoutId;
}
/**
* Returns the checkout type
* @return checkout type
*/
public CheckoutType getCheckoutType() {
return checkoutType;
}
/**
* Returns the user name for the associated checkout.
*/
public String getUser() {
return user;
}
/**
* Returns the file version which was checked-out.
*/
public int getCheckoutVersion() {
return version;
}
/**
* Returns the time at which the checkout was completed.
*/
public long getCheckoutTime() {
return time;
}
/**
* Returns the time at which the checkout was completed.
* @return
*/
public Date getCheckoutDate() {
return new Date(time);
}
/**
* Returns user's local project path if known.
*/
public String getProjectPath() {
return projectPath;
}
/**
* Return a Project location which corresponds to the projectPath
* or null if one can not be constructed.
* @return project location
*/
public String getProjectName() {
if (projectPath == null) {
return null;
}
String path = projectPath;
int ix = path.indexOf("::");
if (ix > 0) {
path = path.substring(ix + 2);
}
ix = path.lastIndexOf('/');
if (ix < 0) {
return null;
}
return path.substring(ix + 1);
}
/**
* Return a Project location which corresponds to the projectPath
* or null if one can not be constructed.
* @return project location
*/
public String getProjectLocation() {
if (projectPath == null) {
return null;
}
String path = projectPath;
int ix = path.indexOf("::");
if (ix > 0) {
path = path.substring(ix + 2);
}
ix = path.lastIndexOf('/');
if (ix < 0) {
return null;
}
return path.substring(0, ix);
}
/**
* Returns the user's hostname associated with the original checkout
* @return host name or null
*/
public String getUserHostName() {
if (projectPath == null) {
return null;
}
int ix = projectPath.indexOf("::");
if (ix > 0) {
return projectPath.substring(0, ix);
}
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (checkoutId ^ (checkoutId >>> 32));
result = prime * result + (int) (time ^ (time >>> 32));
result = prime * result + ((user == null) ? 0 : user.hashCode());
result = prime * result + version;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof ItemCheckoutStatus)) {
return false;
}
ItemCheckoutStatus other = (ItemCheckoutStatus) obj;
return checkoutId == other.checkoutId && user.equals(other.user) &&
version == other.version && time != other.time;
}
/**
* Get project path string suitable for checkout requests
* @param projectPath
* @param isTransient true if project is transient
* @return project location path
*/
public static String getProjectPath(String projectPath, boolean isTransient) {
String hostname = "";
try {
hostname = InetAddress.getLocalHost().getHostName() + "::";
}
catch (UnknownHostException e1) {
hostname = "<standalone>::";
}
if (isTransient) {
return hostname + "<Transient>";
}
return hostname + projectPath;
}
}

View file

@ -0,0 +1,41 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store;
import ghidra.util.exception.UsrException;
/**
* Indicates a failure to obtain a required lock.
*/
public class LockException extends UsrException {
/**
* Construct a new LockException
*/
public LockException() {
super("Operation requires exclusive access to object.");
}
/**
* Construct a new LockException with the given message
* @param msg the exception message
*/
public LockException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,106 @@
/* ###
* 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.store;
import java.io.IOException;
/**
* <code>Version</code> provides immutable information about a specific version of an item.
*/
public class Version implements java.io.Serializable {
public static final long serialVersionUID = 1L;
private static final int VERSION = 1;
private int version;
private long createTime;
private String user;
private String comment;
/**
* Constructor.
* @param version file version number
* @param createTime time version was created
* @param user name of user who created version
* @param comment version comment
*/
public Version(int version, long createTime, String user, String comment) {
this.version = version;
this.createTime = createTime;
this.user = user;
this.comment = comment;
}
/**
* Serialization method
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.writeInt(VERSION);
out.writeInt(version);
out.writeLong(createTime);
out.writeUTF(user);
out.writeUTF(comment != null ? comment : "");
}
/**
* Deserialization method
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
long ver = in.readInt();
if (ver > VERSION) {
throw new ClassNotFoundException("Unsupported version of Version");
}
version = in.readInt();
createTime = in.readLong();
user = in.readUTF();
comment = in.readUTF();
}
/**
* Returns version number.
*/
public int getVersion() {
return version;
}
/**
* Returns time at which version was created.
*/
public long getCreateTime() {
return createTime;
}
/**
* Returns version comment.
*/
public String getComment() {
return comment;
}
/**
* Returns name of user who created version.
*/
public String getUser() {
return user;
}
}

View file

@ -0,0 +1,187 @@
/* ###
* 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.store.db;
import java.io.File;
import java.io.IOException;
import db.DBChangeSet;
import db.DBHandle;
import db.buffers.BufferFile;
import generic.jar.ResourceFile;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
/**
* <code>DBHandle</code> provides access to a PackedDatabase.
*/
public class PackedDBHandle extends DBHandle {
// NOTE: If saveAs is used to save to a non-packed database, pdb will become null and this handle
// should behave like a normal DBHandle
private PackedDatabase pdb;
private String contentType;
/**
* Constructs a temporary packed database handle.
* @param contentType user defined content type.
* @throws IOException
*/
public PackedDBHandle(String contentType) throws IOException {
super();
this.contentType = contentType;
}
/**
* Constructs a database handle for an existing packed database.
* Update mode is determined by bfile.
* @param database packed database
* @param bfile temporary unpacked database which corresponds to the
* specified packed database.
*/
PackedDBHandle(PackedDatabase pdb, BufferFile bfile) throws IOException {
super(bfile);
this.pdb = pdb;
this.contentType = pdb.getContentType();
}
/*
* @see ghidra.framework.store.db.DBHandle#save(java.lang.String, ghidra.framework.model.ChangeSet, ghidra.util.task.TaskMonitor)
*/
@Override
public synchronized void save(String comment, DBChangeSet changeSet, TaskMonitor monitor)
throws IOException, CancelledException {
super.save(comment, changeSet, monitor);
if (pdb != null && !pdb.isReadOnly()) {
pdb.packDatabase(monitor);
}
}
/**
* Saves the open database to the corresponding PackedDatabase file.
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public synchronized void save(TaskMonitor monitor) throws IOException, CancelledException {
save("", null, monitor);
}
@Override
protected synchronized void saveAs(BufferFile outFile, Long newDatabaseId, TaskMonitor monitor)
throws IOException, CancelledException {
super.saveAs(outFile, newDatabaseId, monitor);
if (pdb != null) {
pdb.dispose();
pdb = null;
}
}
@Override
public synchronized void saveAs(BufferFile outFile, boolean associateWithNewFile,
TaskMonitor monitor) throws IOException, CancelledException {
super.saveAs(outFile, associateWithNewFile, monitor);
if (associateWithNewFile && pdb != null) {
pdb.dispose();
pdb = null;
}
}
@Override
public synchronized void close() {
super.close();
if (pdb != null) {
pdb.dispose();
pdb = null;
}
}
/**
* Save open database to a new packed database.
* If another PackedDatabase was associated with this handle prior to this invocation
* it should be disposed to that the underlying database resources can be cleaned-up.
* @param itemName
* @param dir
* @param packedFileName
* @param monitor
* @return new packed Database object now associated with this handle.
* @throws CancelledException if task monitor cancelled operation.
* @throws IOException
* @throws DuplicateFileException
* @see db.DBHandle#saveAs(java.io.File, java.lang.String, ghidra.util.task.TaskMonitor)
*/
public synchronized PackedDatabase saveAs(String itemName, File dir, String packedFileName,
TaskMonitor monitor) throws IOException, DuplicateFileException, CancelledException {
if (isTransactionActive())
throw new IllegalStateException("Can't saveAs during transaction");
ResourceFile packedDbFile = new ResourceFile(new File(dir, packedFileName));
pdb = new PackedDatabase(this, packedDbFile, itemName, null, monitor);
return pdb;
}
/**
* Save open database to a new packed database with a specified newDatabaseId.
* If another PackedDatabase was associated with this handle prior to this invocation
* it should be disposed to that the underlying database resources can be cleaned-up.
* NOTE: This method is intended for use in transforming one database to
* match another existing database.
* @param itemName
* @param dir
* @param packedFileName
* @param newDatabaseId database ID to be forced for new database or null to generate
* new database ID
* @param monitor
* @return new packed Database object now associated with this handle.
* @throws CancelledException if task monitor cancelled operation.
* @throws IOException
* @throws DuplicateFileException
* @see db.DBHandle#saveAs(java.io.File, java.lang.String, ghidra.util.task.TaskMonitor)
*/
public synchronized PackedDatabase saveAs(String itemName, File dir, String packedFileName,
Long newDatabaseId, TaskMonitor monitor)
throws IOException, DuplicateFileException, CancelledException {
if (isTransactionActive())
throw new IllegalStateException("Can't saveAs during transaction");
ResourceFile packedDbFile = new ResourceFile(new File(dir, packedFileName));
pdb = new PackedDatabase(this, packedDbFile, itemName, newDatabaseId, monitor);
return pdb;
}
/**
* Returns user defined content type associated with this handle.
*/
public String getContentType() {
return contentType;
}
/**
* Returns PackedDatabase associated with this handle, or null if
* this is a temporary handle which has not yet been saved to a
* PackedDatabase using saveAs.
*/
public PackedDatabase getPackedDatabase() {
return pdb;
}
}

View file

@ -0,0 +1,874 @@
/* ###
* 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.store.db;
import java.io.*;
import java.util.Date;
import java.util.Random;
import db.DBHandle;
import db.Database;
import db.buffers.BufferFileManager;
import db.buffers.LocalManagedBufferFile;
import generic.jar.ResourceFile;
import ghidra.framework.store.FolderItem;
import ghidra.framework.store.db.PackedDatabaseCache.CachedDB;
import ghidra.framework.store.local.*;
import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
import utilities.util.FileUtilities;
/**
* <code>PackedDatabase</code> provides a packed form of Database
* which compresses a single version into a file.
* <br>
* When opening a packed database, a PackedDBHandle is returned
* after first expanding the file into a temporary Database.
*/
public class PackedDatabase extends Database {
/**
* Presence of the directory lock file will prevent the creation or
* modification of any packed database files contained within that directory
* or any sub-directory.
*/
public static final String READ_ONLY_DIRECTORY_LOCK_FILE = ".dbDirLock";
private static final Random RANDOM = new Random();
private static final String TEMPDB_PREFIX = "tmp";
private static final String TEMPDB_EXT = ".pdb";
private static final String TEMPDB_DIR_PREFIX =
LocalFileSystem.HIDDEN_DIR_PREFIX + TEMPDB_PREFIX;
private static final String TEMPDB_DIR_EXT = TEMPDB_EXT + ".db";
private static final String UPDATE_LOCK_TYPE = "u";
static final int LOCK_TIMEOUT = 30000;
private static final long ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
private static WeakSet<PackedDatabase> pdbInstances;
private ResourceFile packedDbFile;
private boolean isCached;
private String itemName;
private String contentType;
private LockFile packedDbLock;
private LockFile updateLock;
private PackedDBHandle dbHandle;
private long dbTime;
private boolean isReadOnly = false;
/**
* Constructor for an existing packed database which will be unpacked into
* a temporary dbDir.
* @param packedDbFile existing packed database file.
* @throws IOException
*/
private PackedDatabase(ResourceFile packedDbFile) throws IOException {
super(createDBDir(), null, true);
this.packedDbFile = packedDbFile;
bfMgr = new PDBBufferFileManager();
boolean success = false;
try {
isReadOnly = isReadOnlyPDBDirectory(packedDbFile.getParentFile());
if (!isReadOnly) {
updateLock = getUpdateLock(packedDbFile.getFile(false));
packedDbLock = getFileLock(packedDbFile.getFile(false));
}
readContentTypeAndName();
addInstance(this);
success = true;
}
finally {
if (!success) {
dispose();
}
}
}
/**
* Constructor for an existing packed database backed by a unpacking cache
* @param packedDbFile
* @param packedDbLock read lock, null signals read only database
* @param cachedDb
* @param monitor
* @throws CancelledException
* @throws IOException
*/
PackedDatabase(ResourceFile packedDbFile, LockFile packedDbLock, CachedDB cachedDb,
TaskMonitor monitor) throws CancelledException, IOException {
super(cachedDb.dbDir, null, false);
this.packedDbFile = packedDbFile;
this.contentType = cachedDb.contentType;
this.itemName = cachedDb.itemName;
this.dbTime = cachedDb.getLastModified();
this.isCached = true;
bfMgr = new PDBBufferFileManager();
boolean success = false;
try {
this.packedDbLock = packedDbLock;
if (packedDbLock != null) {
updateLock = getUpdateLock(packedDbFile.getFile(false));
}
else {
isReadOnly = true; // signaled by absence of lock
}
if (cachedDb.refreshRequired()) {
refreshUnpacking(monitor);
}
addInstance(this);
success = true;
}
finally {
if (!success) {
dispose();
}
}
}
/**
* Constructor for a new packed database which will be created from an
* open PackedDBHandle.
* @param dbHandle
* @param packedDbFile
* @param itemName
* @param newDatabaseId database ID to be forced for new database or null to generate
* new database ID
* @param monitor
* @throws CancelledException
* @throws IOException
*/
PackedDatabase(PackedDBHandle dbHandle, ResourceFile packedDbFile, String itemName,
Long newDatabaseId, TaskMonitor monitor) throws CancelledException, IOException {
super(createDBDir(), null, true);
bfMgr = new PDBBufferFileManager();
boolean success = false;
try {
this.dbHandle = dbHandle;
this.packedDbFile = packedDbFile;
this.itemName = itemName;
this.contentType = dbHandle.getContentType();
if (isReadOnlyPDBDirectory(packedDbFile.getParentFile())) {
throw new ReadOnlyException(
"Read-only DB directory lock, file update not allowed: " + packedDbFile);
}
updateLock = getUpdateLock(packedDbFile.getFile(false));
packedDbLock = getFileLock(packedDbFile.getFile(false));
if (packedDbFile.exists() || !updateLock.createLock(0, true)) {
throw new DuplicateFileException(packedDbFile + " already exists");
}
LocalManagedBufferFile bfile = new LocalManagedBufferFile(dbHandle.getBufferSize(),
bfMgr, FolderItem.DEFAULT_CHECKOUT_ID);
dbHandle.saveAs(bfile, newDatabaseId, monitor);
packDatabase(monitor);
addInstance(this);
success = true;
}
finally {
if (!success) {
dispose();
}
}
}
public boolean isReadOnly() {
return isReadOnly;
}
/**
* Add new PackedDatabase instance and ensure that all non-disposed
* PackedDatabase instances are properly disposed when the VM shuts-down.
* @param pdb new instance
*/
private static synchronized void addInstance(PackedDatabase pdb) {
if (pdbInstances == null) {
pdbInstances = WeakDataStructureFactory.createCopyOnReadWeakSet();
Thread cleanupThread = new Thread("Packed Database Disposer") {
@Override
public void run() {
for (PackedDatabase pdbInstance : pdbInstances) {
try {
if (pdbInstance.dbHandle != null) {
pdbInstance.dbHandle.close();
}
pdbInstance.dispose();
}
catch (Throwable t) {
// Ignore errors
}
}
}
};
Runtime.getRuntime().addShutdownHook(cleanupThread);
}
pdbInstances.add(pdb);
}
/**
* Remove a PackedDatabase instance after it has been disposed.
* @param pdb disposed instance
*/
private static synchronized void removeInstance(PackedDatabase pdb) {
if (pdbInstances != null) {
pdbInstances.remove(pdb);
}
}
/**
* Get a packed database which whose unpacking will be cached if possible
* @param packedDbFile
* @param monitor
* @return packed database which corresponds to the specified packedDbFile
* @throws IOException
* @throws CancelledException
*/
public static PackedDatabase getPackedDatabase(File packedDbFile, TaskMonitor monitor)
throws IOException, CancelledException {
return getPackedDatabase(packedDbFile, false, monitor);
}
/**
* Get a packed database whose unpacking may be cached if possible
* provided doNotCache is false.
* @param packedDbFile
* @param neverCache if true unpacking will never be cache.
* @param monitor
* @return packed database which corresponds to the specified packedDbFile
* @throws IOException
* @throws CancelledException
*/
public static PackedDatabase getPackedDatabase(File packedDbFile, boolean neverCache,
TaskMonitor monitor) throws IOException, CancelledException {
return getPackedDatabase(new ResourceFile(packedDbFile), neverCache, monitor);
}
/**
* Get a packed database whose unpacking may be cached if possible
* provided doNotCache is false.
* @param packedDbFile
* @param neverCache if true unpacking will never be cache.
* @param monitor
* @return packed database which corresponds to the specified packedDbFile
* @throws IOException
* @throws CancelledException
*/
public static PackedDatabase getPackedDatabase(ResourceFile packedDbFile, boolean neverCache,
TaskMonitor monitor) throws IOException, CancelledException {
if (!neverCache && PackedDatabaseCache.isEnabled()) {
try {
return PackedDatabaseCache.getCache().getCachedDB(packedDbFile, monitor);
}
catch (IOException e) {
Msg.warn(PackedDatabase.class,
"PackedDatabase cache failure for: " + packedDbFile + ", " + e.getMessage());
}
}
return new PackedDatabase(packedDbFile);
}
/**
* Check for the presence of directory read-only lock
* @param directory
* @return true if read-only lock exists+
*/
public static boolean isReadOnlyPDBDirectory(ResourceFile directory) {
File dir = directory.getFile(false);
if (dir == null) {
return true;
}
File readOnlyLockFile = new File(dir, READ_ONLY_DIRECTORY_LOCK_FILE);
if (!readOnlyLockFile.isFile()) {
try {
ResourceFile parentFile = directory.getParentFile();
if (parentFile == null) {
return false;
}
return isReadOnlyPDBDirectory(parentFile);
}
catch (SecurityException e) {
// return true
}
}
return true;
}
@Override
protected void finalize() throws Throwable {
dispose();
}
/**
* Free resources consumed by this object.
* If there is an associated database handle it will be closed.
*/
public void dispose() {
if (!isCached && dbDir != null && dbDir.exists()) {
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
if (!dbDir.renameTo(tmpDbDir)) {
Msg.error(this,
"Failed to dispose PackedDatabase - it may still be in use!\n" + packedDbFile,
new Exception());
return;
}
deleteDir(tmpDbDir);
}
if (dbHandle != null) {
dbHandle = null;
if (updateLock != null && updateLock.haveLock(true)) {
updateLock.removeLock();
}
}
if (packedDbLock != null && packedDbLock.haveLock(true)) {
packedDbLock.removeLock();
}
removeInstance(this);
}
/**
* Get 8-digit random hex value for use in naming temporary files.
* @return random string
*/
static String getRandomString() {
int num = RANDOM.nextInt();
return StringUtilities.pad(Integer.toHexString(num).toUpperCase(), '0', 8);
}
/**
* Creates a temporary directory which will be used for storing
* the unpacked database files.
* @return temporary database directory
* @throws IOException
*/
private static File createDBDir() throws IOException {
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
int tries = 0;
while (tries++ < 10) {
File dir = new File(tmpDir, TEMPDB_DIR_PREFIX + getRandomString() + TEMPDB_DIR_EXT);
if (dir.mkdir()) {
return dir;
}
}
throw new IOException("Unable to create temporary database");
}
/**
* Returns the update lock file for the specified packedFile.
* @param packedFile
*/
private static LockFile getUpdateLock(File packedFile) {
return new LockFile(packedFile.getParentFile(), packedFile.getName(), UPDATE_LOCK_TYPE);
}
/**
* Returns the general lock file for the specified packedFile.
* @param packedFile
*/
static LockFile getFileLock(File packedFile) {
return new LockFile(packedFile.getParentFile(), packedFile.getName());
}
/**
* Returns the user defined content type associated with this database.
*/
public String getContentType() {
return contentType;
}
/**
* Returns the storage file associated with this packed database.
*/
public ResourceFile getPackedFile() {
return packedDbFile;
}
/**
* Deletes the storage file associated with this packed database.
* This method should not be called while the database is open, if
* it is an attempt will be made to close the handle.
* @throws IOException
*/
public void delete() throws IOException {
if (isReadOnly) {
throw new ReadOnlyException(
"Read-only DB directory lock, file removal not allowed: " + packedDbFile);
}
dispose();
lock(updateLock, false, false);
try {
if (packedDbFile.exists() && !packedDbFile.delete()) {
throw new IOException("File is in use or write protected");
}
}
finally {
updateLock.removeLock();
}
}
/**
* Deletes the storage file associated with this packed database.
* @throws IOException
*/
public static void delete(File packedDbFile) throws IOException {
LockFile updateLock = getUpdateLock(packedDbFile);
lock(updateLock, false, false);
try {
if (packedDbFile.exists() && !packedDbFile.delete()) {
throw new IOException("File is in use or write protected");
}
}
finally {
updateLock.removeLock();
}
}
/**
* Obtain a lock on the packed database for reading or writing.
* @param lockFile general or update lock file
* @param wait if true, block until lock is obtained.
* @param hold if true, hold lock until released.
* @throws FileInUseException
*/
static void lock(LockFile lockFile, boolean wait, boolean hold) throws FileInUseException {
if (!lockFile.createLock(wait ? LOCK_TIMEOUT : 0, hold)) {
String msg = "File is in use - '" + lockFile + "'";
String user = lockFile.getLockOwner();
if (user != null) {
msg += " by " + user;
}
throw new FileInUseException(msg);
}
}
/**
* Read user content type and name from packed file.
* @throws IOException
*/
private void readContentTypeAndName() throws IOException {
ItemDeserializer itemDeserializer = null;
if (packedDbLock != null) {
lock(packedDbLock, true, true);
}
try {
itemDeserializer = new ItemDeserializer(packedDbFile);
if (itemDeserializer.getFileType() != FolderItem.DATABASE_FILE_TYPE) {
throw new IOException("Incorrect file type");
}
contentType = itemDeserializer.getContentType();
itemName = itemDeserializer.getItemName();
}
finally {
if (itemDeserializer != null) {
itemDeserializer.dispose();
}
if (packedDbLock != null) {
packedDbLock.removeLock();
}
}
}
/**
* Create a new Database with data provided by an ItemDeserializer.
* @param dir the parent directory which contains the "Hidden" database directory.
* @param dbName the unmangled database name
* @param checkinId
* @param packedFile
* @param monitor
* @throws CancelledException
*/
public static void unpackDatabase(BufferFileManager bfMgr, long checkinId, File packedFile,
TaskMonitor monitor) throws IOException, CancelledException {
if (bfMgr.getCurrentVersion() != 0) {
throw new IllegalStateException("Expected empty database");
}
refreshDatabase(bfMgr, checkinId, new ResourceFile(packedFile), monitor);
}
private static void refreshDatabase(BufferFileManager bfMgr, long checkinId,
ResourceFile packedFile, TaskMonitor monitor) throws IOException, CancelledException {
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}
int version = bfMgr.getCurrentVersion() + 1; // should be 1 in most situations
File file = bfMgr.getBufferFile(version);
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
ItemDeserializer itemDeserializer = null;
try {
Msg.debug(PackedDatabase.class, "Unpacking database " + packedFile + " -> " + file);
itemDeserializer = new ItemDeserializer(packedFile);
itemDeserializer.saveItem(out, monitor);
bfMgr.versionCreated(version, "Unpacked " + packedFile, checkinId);
}
catch (IOCancelledException e) {
throw new CancelledException();
}
finally {
if (itemDeserializer != null) {
itemDeserializer.dispose();
}
try {
out.close();
}
catch (IOException e) {
// ignore
}
}
}
/**
* Refresh the temporary database from the packed file if it has been updated
* since the previous refresh.
* @param monitor
* @return True if refresh was successful or not required.
* False may be returned if refresh failed due to unpacked files being in use.
* @throws IOException
* @throws CancelledException
*/
private boolean refreshUnpacking(TaskMonitor monitor) throws CancelledException, IOException {
monitor.setMessage("Waiting...");
if (!dbDir.isDirectory()) {
throw new IOException("PackedDatabase has been disposed");
}
if (packedDbLock != null) {
lock(packedDbLock, true, true);
}
try {
long modTime = packedDbFile.lastModified();
if (modTime == 0) {
throw new FileNotFoundException("File not found: " + packedDbFile);
}
if (isCached) {
CachedDB entry = PackedDatabaseCache.getCache().getCachedDBEntry(packedDbFile);
if (entry != null && entry.getLastModified() == modTime) {
return true;
}
}
if (dbTime == modTime) {
return true;
}
// File[] files = dbDir.listFiles();
// for (int i = 0; i < files.length; i++) {
// if (!files[i].delete()) {
// return false;
// }
// }
// currentVersion = 0;
monitor.setMessage("Unpacking file...");
refreshDatabase(bfMgr, -1, packedDbFile, monitor);
dbTime = modTime;
if (isCached) {
PackedDatabaseCache.getCache().updateLastModified(packedDbFile, modTime);
}
// currentVersion = 1;
}
finally {
if (packedDbLock != null) {
packedDbLock.removeLock();
}
}
return true;
}
/**
* Serialize (i.e., pack) an open database into the specified outputFile.
* @param dbh open database handle
* @param itemName item name to associate with packed content
* @param contentType supported content type
* @param outputFile packed output file to be created
* @param monitor progress monitor
* @throws IOException
* @throws CancelledException if monitor cancels operation
*/
public static void packDatabase(DBHandle dbh, String itemName, String contentType,
File outputFile, TaskMonitor monitor) throws CancelledException, IOException {
synchronized (dbh) {
if (isReadOnlyPDBDirectory(new ResourceFile(outputFile.getParentFile()))) {
throw new ReadOnlyException(
"Read-only DB directory lock, file creation not allowed: " + outputFile);
}
if (outputFile.exists()) {
throw new DuplicateFileException(outputFile + " already exists");
}
boolean success = false;
InputStream itemIn = null;
File tmpFile = null;
try {
tmpFile = File.createTempFile("pack", ".tmp");
tmpFile.delete();
dbh.saveAs(tmpFile, false, monitor);
itemIn = new BufferedInputStream(new FileInputStream(tmpFile));
try {
ItemSerializer.outputItem(itemName, contentType, FolderItem.DATABASE_FILE_TYPE,
tmpFile.length(), itemIn, outputFile, monitor);
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
}
success = true;
}
finally {
if (itemIn != null) {
try {
itemIn.close();
}
catch (IOException e) {
}
}
tmpFile.delete();
if (!success) {
outputFile.delete();
}
}
}
}
/**
* Create a packed file from an existing Database.
* @param name database name
* @param contentType user content type
* @param bfMgr buffer file manager for existing database
* @param version buffer file version to be packed
* @param outputFile packed storage file to be created
* @param monitor
* @throws IOException
* @throws CancelledException
*/
private static void packDatabase(String name, String contentType, File dbFile, File outputFile,
TaskMonitor monitor) throws IOException, CancelledException {
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}
monitor.setMessage("Packing file...");
InputStream itemIn = new FileInputStream(dbFile);
try {
ItemSerializer.outputItem(name, contentType, FolderItem.DATABASE_FILE_TYPE,
dbFile.length(), itemIn, outputFile, monitor);
}
catch (IOCancelledException e) {
throw new CancelledException();
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
}
}
/**
* Using the temporary unpacked database, update the packed storage file
* using the latest buffer file version.
* @param monitor
* @throws CancelledException
* @throws IOException
*/
void packDatabase(TaskMonitor monitor) throws CancelledException, IOException {
if (isReadOnly || dbHandle == null || bfMgr == null || bfMgr.getCurrentVersion() == 0 ||
!updateLock.haveLock()) {
throw new IOException("Update not allowed");
}
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}
monitor.setMessage("Waiting...");
if (packedDbLock != null) {
lock(packedDbLock, true, true);
}
try {
File packedFile = packedDbFile.getFile(false); // if not read-only packedDbFile must be a file
File dbFile = bfMgr.getBufferFile(bfMgr.getCurrentVersion());
File parentFile = packedFile.getAbsoluteFile().getParentFile();
File tmpFile = File.createTempFile(TEMPDB_PREFIX, TEMPDB_EXT, parentFile);
Msg.debug(PackedDatabase.class, "Packing database " + dbFile + " -> " + packedFile);
packDatabase(itemName, contentType, dbFile, tmpFile, monitor);
File bakFile = new File(parentFile, packedFile.getName() + ".bak");
bakFile.delete();
long oldTime = packedFile.lastModified();
packedFile.renameTo(bakFile);
if (!tmpFile.renameTo(packedFile)) {
bakFile.renameTo(packedFile);
throw new IOException("Update failed for " + packedFile);
}
bakFile.delete();
dbTime = packedFile.lastModified();
if (oldTime == dbTime) {
// ensure that last-modified time on file changes
dbTime += 1000;
packedFile.setLastModified(dbTime);
}
if (isCached) {
try {
PackedDatabaseCache.getCache().updateLastModified(packedDbFile, dbTime);
}
catch (IOException e) {
Msg.warn(this, "cache update failed: " + e.getMessage());
}
}
}
finally {
if (packedDbLock != null) {
packedDbLock.removeLock();
}
}
}
/**
* <code>PDBBufferFileManager</code> removes the update lock when
* the update has completed.
*/
private class PDBBufferFileManager extends DBBufferFileManager {
/*
* @see db.buffers.BufferFileManager#updateEnded(long)
*/
@Override
public void updateEnded(long checkinId) {
dbHandle = null;
if (updateLock != null && updateLock.haveLock(true)) {
updateLock.removeLock();
}
super.updateEnded(checkinId);
}
}
@Override
public synchronized DBHandle open(TaskMonitor monitor) throws CancelledException, IOException {
if (dbHandle != null) {
throw new IOException("Database is already open");
}
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}
if (!refreshUnpacking(monitor)) {
throw new IOException("Failed to unpack/refresh database - it may be in use");
}
LocalManagedBufferFile bfile =
new LocalManagedBufferFile(bfMgr, false, -1, FolderItem.DEFAULT_CHECKOUT_ID);
dbHandle = new PackedDBHandle(this, bfile);
return dbHandle;
}
@Override
public synchronized DBHandle openForUpdate(TaskMonitor monitor)
throws CancelledException, IOException {
if (dbHandle != null) {
throw new IOException("Database is already open");
}
if (isReadOnly) {
throw new ReadOnlyException(
"Read-only DB directory lock, file update not allowed: " + packedDbFile);
}
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}
lock(updateLock, false, true);
boolean success = false;
PackedDBHandle dbh;
try {
if (!refreshUnpacking(monitor)) {
throw new IOException("Failed to unpack/refresh database - it may be in use");
}
LocalManagedBufferFile bfile =
new LocalManagedBufferFile(bfMgr, true, -1, FolderItem.DEFAULT_CHECKOUT_ID);
dbh = new PackedDBHandle(this, bfile);
dbHandle = dbh;
success = true;
}
finally {
if (!success) {
updateLock.removeLock();
}
}
return dbh;
}
/**
* Attempt to remove all old temporary databases.
* Those still open by an existing process should
* not be removed by the operating system.
*/
public static void cleanupOldTempDatabases() {
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
File[] tempDbs = tmpDir.listFiles((FileFilter) file -> {
String name = file.getName();
if (file.isDirectory()) {
if (name.indexOf(TEMPDB_DIR_PREFIX) == 0 && name.endsWith(TEMPDB_DIR_EXT)) {
return true;
}
}
return false;
});
if (tempDbs == null) {
return;
}
// We really have no way of identifying an in-use unpacked database
// so we must give lots of room before removing one (i.e., one week)
long lastWeek = (new Date()).getTime() - ONE_WEEK_MS;
for (File tempDb : tempDbs) {
try {
if (tempDb.isDirectory() && tempDb.lastModified() <= lastWeek) {
if (FileUtilities.deleteDir(tempDb)) {
Msg.info(PackedDatabase.class, "Removed temporary database: " + tempDb);
}
}
}
catch (Exception e) {
}
}
}
}

View file

@ -0,0 +1,496 @@
/* ###
* 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.store.db;
import java.io.*;
import java.util.*;
import db.buffers.LocalBufferFile;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.framework.store.FolderItem;
import ghidra.framework.store.local.ItemDeserializer;
import ghidra.framework.store.local.LockFile;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
public class PackedDatabaseCache {
private static final String CACHE_DIR_PROPERTY = "pdb.cache.dir";
private static final String CACHE_ENABLED_PROPERTY = "pdb.cache.enabled";
private static final int SHELF_LIFE = 7 * 24 * 60 * 60 * 1000; // 7-days
private static final String CACHE_DIR = "packed-db-cache";
private static final String CACHE_MAP_FILE = "cache.map";
private static final String MAP_SEPARATOR = ",";
private static PackedDatabaseCache cache;
private static final String PDB_PREFIX = "pdb";
private static volatile boolean doCleanup = true;
private static Boolean isEnabled;
private final File cacheDir;
private final File mapFile;
private final LockFile lock;
private PackedDatabaseCache(File cacheDir) throws IOException {
this.cacheDir = cacheDir;
if (isEnabled()) {
if (!cacheDir.mkdir() && !cacheDir.isDirectory()) {
throw new IOException("Failed to create cache directory: " + cacheDir);
}
if (!cacheDir.canExecute() || !cacheDir.canWrite()) {
throw new IOException("permission denied: " + cacheDir);
}
Msg.info(this, "Packed database cache: " + cacheDir);
}
else {
Msg.info(this, "Packed database cache is disabled");
}
mapFile = new File(cacheDir, CACHE_MAP_FILE);
lock = new LockFile(cacheDir, "cache", "u");
}
public static boolean isEnabled() {
if (isEnabled == null) {
isEnabled = true;
String enabled = System.getProperty(CACHE_ENABLED_PROPERTY);
if (enabled != null) {
enabled = enabled.trim().toLowerCase();
isEnabled = "true".equals(enabled) || "yes".equals(enabled);
}
}
return isEnabled;
}
public static synchronized PackedDatabaseCache getCache() throws IOException {
if (cache == null) {
File cacheDir = null;
String dirpath = System.getProperty(CACHE_DIR_PROPERTY);
if (dirpath != null) {
cacheDir = new File(dirpath);
}
else {
cacheDir = new File(Application.getUserCacheDirectory(), CACHE_DIR);
}
cache = new PackedDatabaseCache(cacheDir);
}
return cache;
}
private List<CachedDB> readCache() throws IOException {
List<CachedDB> list = new ArrayList<CachedDB>();
if (!mapFile.exists()) {
// cleanup db directories if map is missing
for (File f : cacheDir.listFiles()) {
if (f.isDirectory() && f.getName().startsWith(PDB_PREFIX)) {
FileUtilities.deleteDir(f);
}
}
return list;
}
boolean modified = false;
long now = (new Date()).getTime();
BufferedReader r = new BufferedReader(new FileReader(mapFile));
try {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.length() == 0) {
continue;
}
CachedDB entry = new CachedDB(line);
if (isBadDBDir(entry)) {
Msg.warn(this,
"Forcing removal of bad cached DB: " + entry.itemName + ", " + entry.dbDir);
entry.lastAccessTime = 0; // force cleanup
}
long timeSinceLastAccess = now - entry.lastAccessTime;
if (timeSinceLastAccess > SHELF_LIFE || !entry.dbDir.exists() ||
(entry.refreshRequired() && !entry.originalPackedDBExists())) {
if (doCleanup) {
FileUtilities.deleteDir(entry.dbDir);
modified = true;
}
continue;
}
list.add(entry);
}
}
catch (IllegalArgumentException e) {
Msg.error(this, "Corrupt cache - exit and try removing it: " + cacheDir);
}
finally {
r.close();
}
doCleanup = false;
if (modified) {
writeCacheList(list);
}
return list;
}
private boolean isBadDBDir(CachedDB entry) {
File dbDir = entry.dbDir;
File[] files = dbDir.listFiles();
if (files == null) {
Msg.debug(this, "CachedDB directory not found: " + entry.itemName + ", " + entry.dbDir);
return true;
}
if (files.length == 0) {
// missing/empty directory indicates not yet opened/unpacked
entry.lastModifiedTime = 0;
if (!entry.originalPackedDBExists()) {
Msg.debug(this, "CachedDB has empty directory and packed file not found: " +
entry.itemName + ", " + entry.packedDbFilePath);
return true;
}
return false;
}
for (File f : files) {
if (f.getName().endsWith(LocalBufferFile.BUFFER_FILE_EXTENSION) && f.length() != 0) {
return false;
}
}
Msg.debug(this, "CachedDB is not empty but contains no *.gbf files: " + entry.itemName +
", " + entry.packedDbFilePath);
return true;
}
private void writeCacheList(List<CachedDB> list) throws IOException {
FileOutputStream out = new FileOutputStream(mapFile);
PrintWriter w = new PrintWriter(out);
for (CachedDB entry : list) {
w.println(entry.getMapEntry());
}
try {
out.getFD().sync();
}
catch (SyncFailedException e) {
// Sync not supported - we tried our best
}
w.close();
}
private void addCacheMapEntry(CachedDB cachedDb) throws IOException {
FileOutputStream out = new FileOutputStream(mapFile, true);
PrintWriter w = new PrintWriter(out);
w.println(cachedDb.getMapEntry());
try {
out.getFD().sync();
}
catch (SyncFailedException e) {
// Sync not supported - we tried our best
}
w.close();
}
private CachedDB createCachedDb(ResourceFile packedDbFile) throws IOException {
File dbDir = createCachedDir();
ItemDeserializer itemDeserializer = null;
boolean success = false;
try {
long dbTime = packedDbFile.lastModified();
if (dbTime == 0 || !packedDbFile.isFile()) {
throw new FileNotFoundException("File not found: " + packedDbFile);
}
itemDeserializer = new ItemDeserializer(packedDbFile);
if (itemDeserializer.getFileType() != FolderItem.DATABASE_FILE_TYPE) {
throw new IOException("Incorrect file type");
}
String contentType = itemDeserializer.getContentType();
String itemName = itemDeserializer.getItemName();
CachedDB cachedDb = new CachedDB(packedDbFile, dbDir, contentType, itemName, 0, true);
success = true;
return cachedDb;
}
finally {
if (itemDeserializer != null) {
itemDeserializer.dispose();
}
if (!success) {
FileUtilities.deleteDir(dbDir);
}
}
}
private File createCachedDir() throws IOException {
int tries = 0;
while (tries++ < 10) {
File dir = new File(cacheDir, PDB_PREFIX + PackedDatabase.getRandomString());
if (dir.mkdir()) {
return dir;
}
}
throw new IOException("Unable to create cached database");
}
CachedDB getCachedDBEntry(ResourceFile packedDbFile) throws IOException {
if (!isEnabled()) {
throw new IOException("Cache disabled");
}
if (!lock.createLock(PackedDatabase.LOCK_TIMEOUT, true)) {
throw new IOException("Packed database cache timeout");
}
try {
String packedFilePath = packedDbFile.getCanonicalPath();
List<CachedDB> list = readCache();
for (CachedDB entry : list) {
if (packedFilePath.equals(entry.packedDbFilePath)) {
return entry;
}
}
}
finally {
lock.removeLock();
}
return null;
}
void purgeFromCache(ResourceFile packedDbFile) throws IOException {
if (!lock.createLock(PackedDatabase.LOCK_TIMEOUT, true)) {
throw new IOException("Packed database cache timeout");
}
try {
String packedFilePath = packedDbFile.getCanonicalPath();
List<CachedDB> list = readCache();
for (CachedDB entry : list) {
if (packedFilePath.equals(entry.packedDbFilePath)) {
entry.lastAccessTime = 0;
writeCacheList(list);
FileUtilities.deleteDir(entry.dbDir);
break;
}
}
}
finally {
lock.removeLock();
}
}
boolean isInCache(ResourceFile packedDbFile) throws IOException {
if (!lock.createLock(PackedDatabase.LOCK_TIMEOUT, true)) {
throw new IOException("Packed database cache timeout");
}
try {
String packedFilePath = packedDbFile.getCanonicalPath();
List<CachedDB> list = readCache();
for (CachedDB entry : list) {
if (packedFilePath.equals(entry.packedDbFilePath) && entry.dbDir.isDirectory()) {
return true;
}
}
}
finally {
lock.removeLock();
}
return false;
}
/**
* Get cached packed database
* @param packedDbFile
* @param isReadOnly
* @param monitor
* @return
* @throws CancelledException
* @throws IOException
*/
PackedDatabase getCachedDB(ResourceFile packedDbFile, TaskMonitor monitor)
throws CancelledException, IOException {
if (!isEnabled()) {
throw new IOException("Cache disabled");
}
if (!lock.createLock(PackedDatabase.LOCK_TIMEOUT, true)) {
throw new IOException("Packed database cache timeout");
}
try {
boolean isReadOnly =
PackedDatabase.isReadOnlyPDBDirectory(packedDbFile.getParentFile());
LockFile packedDbLock = null;
if (!isReadOnly) {
packedDbLock = PackedDatabase.getFileLock(packedDbFile.getFile(false));
PackedDatabase.lock(packedDbLock, true, true);
}
try {
String packedFilePath = packedDbFile.getCanonicalPath();
long now = (new Date()).getTime();
CachedDB cachedDb = null;
List<CachedDB> list = readCache();
for (CachedDB entry : list) {
if (packedFilePath.equals(entry.packedDbFilePath)) {
if (!entry.dbDir.canExecute() || !entry.dbDir.canWrite()) {
throw new IOException("Permssion denied: " + entry.dbDir);
}
entry.lastAccessTime = now;
writeCacheList(list);
cachedDb = entry;
break;
}
}
if (cachedDb == null) {
cachedDb = createCachedDb(packedDbFile);
cachedDb.lastAccessTime = now;
addCacheMapEntry(cachedDb);
Msg.debug(this, "Caching packed database: " + cachedDb.packedDbFilePath);
}
else {
Msg.debug(this, "Using cached packed database: " + cachedDb.packedDbFilePath);
}
return new PackedDatabase(packedDbFile, packedDbLock, cachedDb, monitor);
}
finally {
if (packedDbLock != null && packedDbLock.haveLock()) {
// packed database lock may have been removed if disposed on error
packedDbLock.removeLock();
}
}
}
finally {
lock.removeLock();
}
}
void updateLastModified(ResourceFile packedDbFile, long modTime) throws IOException {
if (!isEnabled()) {
throw new IOException("Cache disabled");
}
if (!lock.createLock(PackedDatabase.LOCK_TIMEOUT, true)) {
throw new IOException("Packed database cache timeout");
}
try {
String packedFilePath = packedDbFile.getCanonicalPath();
long now = (new Date()).getTime();
List<CachedDB> list = readCache();
for (CachedDB entry : list) {
if (packedFilePath.equals(entry.packedDbFilePath)) {
entry.lastAccessTime = now;
entry.lastModifiedTime = modTime;
writeCacheList(list);
Msg.debug(this, "Cache update completed: " + packedFilePath);
return;
}
}
Msg.debug(this, "Cache entry not found for: " + packedFilePath);
}
finally {
lock.removeLock();
}
}
class CachedDB {
public final String packedDbFilePath;
public final String itemName;
public final String contentType;
public final File dbDir;
private ResourceFile packedDbFile;
private boolean refreshRequired; // signal PackedDatabase to unpack
private long lastModifiedTime;
private long lastAccessTime;
CachedDB(ResourceFile packedDbFile, File dbDir, String contentType, String itemName,
long lastModifiedTime, boolean refreshRequired) throws IOException {
this.packedDbFile = packedDbFile;
this.packedDbFilePath = packedDbFile.getCanonicalPath();
this.dbDir = dbDir;
this.contentType = contentType;
this.itemName = itemName;
this.lastModifiedTime = lastModifiedTime;
this.refreshRequired = refreshRequired;
}
CachedDB(String mapEntry) {
String[] split = splitEntry(mapEntry);
packedDbFilePath = split[0];
dbDir = new File(cacheDir, split[1]);
lastModifiedTime = Long.parseUnsignedLong(split[2], 16);
contentType = split[3];
itemName = split[4];
try {
packedDbFile = new ResourceFile(packedDbFilePath);
refreshRequired = lastModifiedTime != packedDbFile.lastModified();
}
catch (Exception e) {
// ignore - treat as non-existing file
}
lastAccessTime = Long.parseUnsignedLong(split[5], 16);
}
String[] splitEntry(String mapEntry) {
String[] split = new String[6];
int lastIndex = mapEntry.length();
for (int i = 5; i > 0; i--) {
int index = mapEntry.lastIndexOf(MAP_SEPARATOR, lastIndex - 1);
if (index <= 0 || (lastIndex - index) == 1) {
throw new IllegalArgumentException("Invalid cache map entry");
}
split[i] = mapEntry.substring(index + 1, lastIndex);
lastIndex = index;
}
split[0] = mapEntry.substring(0, lastIndex);
return split;
}
boolean refreshRequired() {
return refreshRequired;
}
long getLastModified() {
return lastModifiedTime;
}
boolean originalPackedDBExists() {
return packedDbFile != null && packedDbFile.isFile();
}
String getMapEntry() {
StringBuffer buf = new StringBuffer();
buf.append(packedDbFilePath);
buf.append(MAP_SEPARATOR);
buf.append(dbDir.getName());
buf.append(MAP_SEPARATOR);
buf.append(Long.toHexString(lastModifiedTime));
buf.append(MAP_SEPARATOR);
buf.append(contentType);
buf.append(MAP_SEPARATOR);
buf.append(itemName);
buf.append(MAP_SEPARATOR);
buf.append(Long.toHexString(lastAccessTime));
return buf.toString();
}
}
}

View file

@ -0,0 +1,341 @@
/* ###
* 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.store.db;
import java.io.*;
import db.*;
import db.buffers.*;
import ghidra.framework.store.local.ItemSerializer;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>PrivateDatabase</code> corresponds to a non-versioned database.
*/
public class PrivateDatabase extends Database {
/**
* Constructor used to create an empty "Non-Versioned" database.
* @param dbDir database directory
* @param dbFileListener database listener which will be notified when
* initial version is created.
* @throws IOException
*/
private PrivateDatabase(File dbDir, DBFileListener dbFileListener) throws IOException {
super(dbDir, dbFileListener, true);
}
/**
* Constructor for an existing "Non-Versioned" Database.
* @param dbDir database directory
* @throws IOException
*/
public PrivateDatabase(File dbDir) throws IOException {
super(dbDir);
}
/**
* Construct a new Database from an existing srcFile.
* @param dbDir
* @param srcFile
* @param resetDatabaseId if true database ID will be reset for new Database
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public PrivateDatabase(File dbDir, BufferFile srcFile, boolean resetDatabaseId,
TaskMonitor monitor) throws IOException, CancelledException {
super(dbDir, null, true);
boolean success = false;
LocalBufferFile newFile = null;
try {
newFile = new LocalManagedBufferFile(srcFile.getBufferSize(), bfMgr, -1);
LocalBufferFile.copyFile(srcFile, newFile, null, monitor);
newFile.close(); // causes create notification
if (resetDatabaseId) {
DBHandle.resetDatabaseId(bfMgr.getBufferFile(1));
}
success = true;
}
finally {
if (!success) {
if (newFile != null) {
newFile.delete();
}
if (dbDirCreated) {
deleteDir(dbDir);
}
}
}
}
/**
* Constructs a new Database from an existing packed database file.
* @param dbDir private database directory
* @param packedFile packed database storage file
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public PrivateDatabase(File dbDir, File packedFile, TaskMonitor monitor)
throws IOException, CancelledException {
super(dbDir, null, true);
boolean success = false;
try {
PackedDatabase.unpackDatabase(bfMgr, -1, packedFile, monitor);
DBHandle.resetDatabaseId(bfMgr.getBufferFile(1));
success = true;
}
finally {
if (!success) {
if (dbDirCreated) {
deleteDir(dbDir);
}
}
}
}
/**
* Create a new database and provide the initial buffer file for writing.
* @param dbDir
* @param bufferSize
* @return initial buffer file
* @throws IOException
*/
public static LocalManagedBufferFile createDatabase(File dbDir, DBFileListener dbFileListener,
int bufferSize) throws IOException {
PrivateDatabase db = new PrivateDatabase(dbDir, dbFileListener);
boolean success = false;
try {
LocalManagedBufferFile bfile = new LocalManagedBufferFile(bufferSize, db.bfMgr, -1);
success = true;
return bfile;
}
finally {
if (!success && db.dbDirCreated) {
deleteDir(dbDir);
}
}
}
/**
* If this is a checked-out copy and a cumulative change file
* should be maintained, this method must be invoked following
* construction.
*/
public void setIsCheckoutCopy(boolean state) {
isCheckOutCopy = state;
}
/**
* Open the current version of this database for non-update use.
* @return buffer file for non-update use
* @throws IOException
*/
public LocalManagedBufferFile openBufferFile() throws IOException {
synchronized (syncObject) {
return new LocalManagedBufferFile(bfMgr, false, -1, -1);
}
}
/**
* Open the current version of this database for update use.
* @param recover if true an attempt will be made to recover unsaved changes
* from a previous crash.
* @param monitor task monitor
* @return updateable buffer file
* @throws IOException
*/
public LocalManagedBufferFile openBufferFileForUpdate() throws IOException {
if (!updateAllowed) {
throw new IOException("Update use not permitted");
}
synchronized (syncObject) {
return new LocalManagedBufferFile(bfMgr, true, -1, -1);
}
}
/**
* Returns true if recovery data exists which may enable recovery of unsaved changes
* resulting from a previous crash.
*/
public boolean canRecover() {
return BufferMgr.canRecover(bfMgr);
}
/**
* Following a move of the database directory,
* this method should be invoked if this instance will
* continue to be used.
* @param dirir new database directory
*/
public void dbMoved(File dir) throws FileNotFoundException {
synchronized (syncObject) {
this.dbDir = dir;
refresh();
}
}
/**
* If this is a checked-out copy, replace the buffer file content with that
* provided by the specified srcFile. This Database must be a checkout copy.
* If a cumulative change files exists, it will be deleted following the update.
* @param srcFile open source data buffer file or null if current version
* is already up-to-date.
* @param oldVersion older version of srcFile from which this database originated.
* @throws IOException
* @throws CancelledException
*/
public void updateCheckoutCopy(ManagedBufferFile srcFile, int oldVersion, TaskMonitor monitor)
throws CancelledException, IOException {
if (!isCheckOutCopy) {
throw new IOException("Database is not a checkout copy");
}
synchronized (syncObject) {
if (srcFile != null) {
boolean success = false;
// TODO: watch-out for multiple updatable BufferFile instances
LocalManagedBufferFile localBf = new LocalManagedBufferFile(bfMgr, true, -1, -1);
try {
localBf.updateFrom(srcFile, oldVersion, monitor); // performs a save
localBf.close();
success = true;
}
finally {
if (!success) {
localBf.delete();
}
}
}
(new File(dbDir, CUMULATIVE_CHANGE_FILENAME)).delete();
(new File(dbDir, CUMULATIVE_MODMAP_FILENAME)).delete();
}
}
/**
* If a cumulative change files exists, it will be deleted.
* @throws IOException
* @throws CancelledException
*/
public void updateCheckoutCopy() throws CancelledException, IOException {
if (!isCheckOutCopy) {
throw new IOException("Database is not a checkout copy");
}
synchronized (syncObject) {
(new File(dbDir, CUMULATIVE_CHANGE_FILENAME)).delete();
(new File(dbDir, CUMULATIVE_MODMAP_FILENAME)).delete();
}
}
/**
* Move the content of the otherDb into this database.
* The otherDb will no longer exist if this method is successful.
* If already open for update, a save should not be done or the database
* may become corrupted. All existing handles should be closed and reopened
* when this method is complete.
* @param privateDb
* @throws IOException if an IO error occurs. An attempt will be made to restore
* this database to its original state, however the otherDb will not be repaired
* and may become unusable.
*/
public void updateCheckoutFrom(PrivateDatabase otherDb) throws IOException {
if (!isCheckOutCopy) {
throw new IOException("Database is not a checkout copy");
}
synchronized (syncObject) {
int newVersion = currentVersion + 1;
File otherBufFile = otherDb.bfMgr.getBufferFile(otherDb.currentVersion);
File otherChangeFile = new File(otherDb.dbDir, CUMULATIVE_CHANGE_FILENAME);
File otherMapFile = new File(otherDb.dbDir, CUMULATIVE_MODMAP_FILENAME);
File newBufFile = bfMgr.getBufferFile(newVersion);
File changeFile = new File(dbDir, CUMULATIVE_CHANGE_FILENAME);
File mapFile = new File(dbDir, CUMULATIVE_MODMAP_FILENAME);
File backupChangeFile = new File(dbDir, CUMULATIVE_CHANGE_FILENAME + ".bak");
File backupMapFile = new File(dbDir, CUMULATIVE_MODMAP_FILENAME + ".bak");
backupMapFile.delete();
backupChangeFile.delete();
if (!otherBufFile.exists()) {
throw new IOException("Update file not found");
}
if (newBufFile.exists() || !otherBufFile.renameTo(newBufFile)) {
throw new IOException("Concurrent database modification error (1)");
}
boolean success = false;
try {
if (mapFile.exists() && !mapFile.renameTo(backupMapFile)) {
throw new IOException("Concurrent database modification error (2)");
}
if (changeFile.exists() && !changeFile.renameTo(backupChangeFile)) {
throw new IOException("Concurrent database modification error (3)");
}
if (!otherMapFile.renameTo(mapFile) || !otherChangeFile.renameTo(changeFile)) {
throw new IOException("Concurrent database modification error (4)");
}
currentVersion = newVersion;
lastModified = newBufFile.lastModified();
success = true;
}
finally {
if (!success) {
newBufFile.delete();
mapFile.delete();
backupMapFile.renameTo(mapFile);
changeFile.delete();
backupChangeFile.renameTo(changeFile);
}
else {
backupChangeFile.delete();
backupMapFile.delete();
}
}
}
}
/**
* Output the current version of this database to a packed storage file.
* @param outputFile packed storage file to be written
* @param name database name
* @param filetype application file type
* @param contentType user content type
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public void output(File outputFile, String name, int filetype, String contentType,
TaskMonitor monitor) throws IOException, CancelledException {
synchronized (syncObject) {
File file = bfMgr.getBufferFile(currentVersion);
InputStream itemIn = new BufferedInputStream(new FileInputStream(file));
try {
ItemSerializer.outputItem(name, contentType, filetype, file.length(), itemIn,
outputFile, monitor);
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
}
}
}
}

View file

@ -0,0 +1,70 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.db;
import java.io.IOException;
/**
* <code>VersionedDBListener</code> provides listeners the ability to be notified
* when changes occur to a versioned database.
*/
public interface VersionedDBListener {
/**
* Available database versions have been modified.
* This method is not invoked when a new version is created.
* @param minVersion minimum available version
* @param currentVersion current/latest version
*/
public void versionsChanged(int minVersion, int currentVersion);
/**
* A new database version has been created.
* @param db
* @param version
* @param time
* @param comment
* @param checkinId
* @return true if version is allowed, if false is returned
* the version will be removed.
*/
public boolean versionCreated(VersionedDatabase db, int version, long time, String comment,
long checkinId);
/**
* A version has been deleted.
* @param version
*/
public void versionDeleted(int version);
/**
* Returns the checkout version associated with the specified
* checkoutId. A returned version of -1 indicates that the
* checkoutId is not valid.
* @param checkoutId
* @return checkout version
*/
public int getCheckoutVersion(long checkoutId) throws IOException;
/**
* Terminate the specified checkout.
* A new version may or may not have been created.
* @param checkoutId
*/
public void checkinCompleted(long checkoutId);
}

View file

@ -0,0 +1,543 @@
/* ###
* 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.store.db;
import java.io.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import db.DBHandle;
import db.Database;
import db.buffers.*;
import ghidra.framework.store.local.ItemSerializer;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
* <code>VersionedDatabase</code> corresponds to a versioned database.
*/
public class VersionedDatabase extends Database {
static final Logger log = LogManager.getLogger(VersionedDatabase.class);
public final int LATEST_VERSION = -1;
public static final long DEFAULT_CHECKOUT_ID = -1;
/**
* Change listener
*/
protected VersionedDBListener verDBListener;
/**
* General "Versioned" Database Constructor.
* @param dbDir
* @param verDBListener
* @param create if true an empty database will be created.
* @throws IOException
*/
private VersionedDatabase(File dbDir, VersionedDBListener verDBListener, boolean create)
throws IOException {
super(dbDir, true, create);
this.verDBListener = verDBListener;
bfMgr = new VerDBBufferFileManager();
scanFiles(true);
if (create && currentVersion != 0) {
throw new IOException("Database already exists");
}
if (!create && currentVersion == 0) {
throw new FileNotFoundException("Database files not found");
}
}
/**
* Constructor for an existing "Versioned" Database.
* @param dbDir database directory
* @param verDBListener
* @throws IOException
*/
public VersionedDatabase(File dbDir, VersionedDBListener verDBListener) throws IOException {
this(dbDir, verDBListener, false);
}
/**
* Construct a new "Versioned" Database from an existing srcFile.
* @param dbDir
* @param srcFile
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public VersionedDatabase(File dbDir, BufferFile srcFile, VersionedDBListener verDBListener,
long checkoutId, String comment, TaskMonitor monitor)
throws IOException, CancelledException {
this(dbDir, verDBListener, true);
boolean success = false;
LocalManagedBufferFile newFile = null;
try {
if (verDBListener.getCheckoutVersion(checkoutId) != 0) {
throw new IOException("Expected checkout version of 0");
}
newFile = new LocalManagedBufferFile(srcFile.getBufferSize(), bfMgr, checkoutId);
newFile.setVersionComment(comment);
LocalBufferFile.copyFile(srcFile, newFile, null, monitor);
newFile.close(); // causes create notification
success = true;
}
finally {
if (!success) {
if (newFile != null) {
newFile.delete();
}
if (dbDirCreated) {
deleteDir(dbDir);
}
}
}
}
/**
* Construct a new "Versioned" Database from a packed database file
* @param dbDir
* @param packedFile
* @param verDBListener
* @param checkoutId
* @param comment
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public VersionedDatabase(File dbDir, File packedFile, VersionedDBListener verDBListener,
long checkoutId, String comment, TaskMonitor monitor)
throws IOException, CancelledException {
this(dbDir, verDBListener, true);
boolean success = false;
try {
if (verDBListener.getCheckoutVersion(checkoutId) != 0) {
throw new IOException("Expected checkout version of 0");
}
PackedDatabase.unpackDatabase(bfMgr, checkoutId, packedFile, monitor);
success = true;
}
finally {
if (!success) {
if (dbDirCreated) {
deleteDir(dbDir);
}
}
}
}
/**
* Create a new database and provide the initial buffer file for writing.
* @param dbDir
* @param bufferSize
* @return initial buffer file
* @throws IOException
*/
public static LocalManagedBufferFile createVersionedDatabase(File dbDir, int bufferSize,
VersionedDBListener verDBListener, long checkoutId) throws IOException {
VersionedDatabase db = new VersionedDatabase(dbDir, verDBListener, true);
boolean success = false;
try {
LocalManagedBufferFile bfile =
new LocalManagedBufferFile(bufferSize, db.bfMgr, checkoutId);
success = true;
return bfile;
}
finally {
if (!success && db.dbDirCreated) {
deleteDir(dbDir);
}
}
}
/**
* Returns the version number associated with the oldest buffer file version.
*/
public int getMinimumVersion() {
synchronized (syncObject) {
return minVersion;
}
}
/**
* Returns the version number associated with the latest buffer file version.
*/
@Override
public int getCurrentVersion() {
synchronized (syncObject) {
return currentVersion;
}
}
/**
* Delete oldest version.
* @throws IOException if an error occurs or this is the only version.
*/
public void deleteMinimumVersion() throws IOException {
synchronized (syncObject) {
if (minVersion == currentVersion) {
throw new IOException("Unable to delete last remaining version");
}
// Rename previous version/change files
File versionFile = bfMgr.getVersionFile(minVersion);
File changeFile = bfMgr.getChangeDataFile(minVersion);
File delVersionFile =
new File(versionFile.getParentFile(), versionFile.getName() + ".delete");
File delChangeFile =
new File(changeFile.getParentFile(), changeFile.getName() + ".delete");
delVersionFile.delete();
delChangeFile.delete();
if (!versionFile.renameTo(delVersionFile)) {
throw new FileInUseException("Version " + minVersion + " is in use");
}
else if (!changeFile.renameTo(delChangeFile)) {
delVersionFile.renameTo(versionFile);
throw new FileInUseException("Version " + minVersion + " is in use");
}
// Complete removal
delVersionFile.delete();
delChangeFile.delete();
int deletedVersion = minVersion++;
verDBListener.versionDeleted(deletedVersion);
}
}
/**
* Delete latest version.
* @throws IOException if an error occurs or this is the only version.
*/
public void deleteCurrentVersion() throws IOException {
synchronized (syncObject) {
if (minVersion == currentVersion) {
throw new IOException("Unable to delete last remaining version");
}
// Re-build buffer file for (currentVersion-1)
int prevVer = currentVersion - 1;
File prevBFile = bfMgr.getBufferFile(prevVer);
if (!prevBFile.exists()) {
LocalBufferFile srcBf = openBufferFile(prevVer, -1);
try {
srcBf.clone(prevBFile, null);
}
catch (CancelledException e) {
throw new AssertException();
}
finally {
try {
srcBf.close();
}
catch (IOException e) {
// ignore
}
}
}
// Rename previous version/change files
File versionFile = bfMgr.getVersionFile(prevVer);
File changeFile = bfMgr.getChangeDataFile(prevVer);
File delVersionFile =
new File(versionFile.getParentFile(), versionFile.getName() + ".delete");
File delChangeFile =
new File(changeFile.getParentFile(), changeFile.getName() + ".delete");
delVersionFile.delete();
delChangeFile.delete();
if (!versionFile.renameTo(delVersionFile)) {
throw new FileInUseException("Version " + prevVer + " is in use");
}
else if (!changeFile.renameTo(delChangeFile)) {
delVersionFile.renameTo(versionFile);
throw new FileInUseException("Version " + prevVer + " is in use");
}
// Remove current version
if (!bfMgr.getBufferFile(currentVersion).delete()) {
prevBFile.delete();
delVersionFile.renameTo(versionFile);
delChangeFile.renameTo(changeFile);
throw new FileInUseException("Version " + currentVersion + " is in use");
}
// Complete removal
delVersionFile.delete();
delChangeFile.delete();
int deletedVersion = currentVersion--;
verDBListener.versionDeleted(deletedVersion);
}
}
/**
* Open a specific version of this database for non-update use.
* @param version database version or LATEST_VERSION for current version
* @param minChangeDataVer the minimum database version whoose change data
* should be associated with the returned buffer file. A value of -1 indicates that
* change data is not required.
* @return buffer file for non-update use.
* @throws IOException
*/
public LocalManagedBufferFile openBufferFile(int version, int minChangeDataVer)
throws IOException {
synchronized (syncObject) {
if (version != LATEST_VERSION && (version > currentVersion || version < minVersion)) {
throw new FileNotFoundException(
"Version " + version + " not available for " + dbDir);
}
if (version == currentVersion || version == LATEST_VERSION) {
return new LocalManagedBufferFile(bfMgr, false, minChangeDataVer,
DEFAULT_CHECKOUT_ID);
}
return new LocalManagedBufferFile(bfMgr, version, minChangeDataVer);
}
}
/**
* Open a specific version of the stored database for non-update use.
* The returned handle does not support the Save operation.
* @param version database version
* @param monitor task monitor (may be null)
* @return database handle
* @throws FileInUseException thrown if unable to obtain the required database lock(s).
* @throws IOException thrown if IO error occurs.
*/
public DBHandle open(int version, int minChangeDataVer, TaskMonitor monitor)
throws IOException {
synchronized (syncObject) {
return new DBHandle(openBufferFile(version, minChangeDataVer));
}
}
/*
* @see db.Database#openForUpdate(ghidra.util.task.TaskMonitor)
*/
@Override
public DBHandle openForUpdate(TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException();
}
/**
* Open the current version of this database for update use.
* @param checkoutId checkout ID
* @return updateable buffer file
* @throws IOException if update not permitted or other error occurs
*/
public LocalManagedBufferFile openBufferFileForUpdate(long checkoutId) throws IOException {
if (!updateAllowed) {
throw new IOException("Update use not permitted");
}
synchronized (syncObject) {
int minChangeDataVer = verDBListener.getCheckoutVersion(checkoutId);
if (minChangeDataVer < 0) {
throw new IOException("Checkout not found");
}
return new LocalManagedBufferFile(bfMgr, true, minChangeDataVer, checkoutId);
}
}
/**
* Following a move of the database directory,
* this method should be invoked if this instance will
* continue to be used.
* @param dbDir new database directory
*/
public void dbMoved(File dbDir) throws FileNotFoundException {
synchronized (syncObject) {
this.dbDir = dbDir;
refresh();
}
}
/**
* Scan files and update state.
* @param repair if true files are repaired if needed.
*/
@Override
protected void scanFiles(boolean repair) throws FileNotFoundException {
synchronized (syncObject) {
super.scanFiles(repair);
if (currentVersion != 0 && repair) {
verDBListener.versionsChanged(minVersion, currentVersion);
}
}
}
/**
* Output the current version of this database to a packed storage file.
* @param outputFile packed storage file to be written
* @param name database name
* @param filetype application file type
* @param contentType user content type
* @param monitor
* @throws IOException
* @throws CancelledException
*/
public void output(int version, File outputFile, String name, int filetype, String contentType,
TaskMonitor monitor) throws IOException, CancelledException {
synchronized (syncObject) {
if (outputFile.exists()) {
throw new DuplicateFileException(outputFile.getName() + " already exists");
}
if (version == LATEST_VERSION || version == currentVersion) {
File file = bfMgr.getBufferFile(currentVersion);
InputStream itemIn = new BufferedInputStream(new FileInputStream(file));
boolean success = false;
try {
ItemSerializer.outputItem(name, contentType, filetype, file.length(), itemIn,
outputFile, monitor);
success = true;
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
if (!success) {
outputFile.delete();
}
}
}
else {
BufferFile bf = openBufferFile(version, -1);
try {
File tmpFile = File.createTempFile("ghidra", LocalBufferFile.TEMP_FILE_EXT);
tmpFile.delete();
BufferFile tmpBf = new LocalBufferFile(tmpFile, bf.getBufferSize());
boolean success = false;
try {
LocalBufferFile.copyFile(bf, tmpBf, null, monitor);
tmpBf.close();
InputStream itemIn = new FileInputStream(tmpFile);
try {
ItemSerializer.outputItem(name, contentType, filetype, tmpFile.length(),
itemIn, outputFile, monitor);
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
}
success = true;
}
finally {
if (!success) {
outputFile.delete();
}
tmpBf.close();
tmpFile.delete();
}
}
finally {
bf.close();
}
}
}
}
/**
* <code>VerDBBufferFileManager</code> provides buffer file management
* for this versioned database instead of the DBBufferFileManager.
*/
private class VerDBBufferFileManager implements BufferFileManager {
@Override
public int getCurrentVersion() {
synchronized (syncObject) {
return currentVersion;
}
}
@Override
public File getBufferFile(int version) {
return new File(dbDir,
DATABASE_FILE_PREFIX + version + LocalBufferFile.BUFFER_FILE_EXTENSION);
}
@Override
public File getVersionFile(int version) {
return new File(dbDir,
VERSION_FILE_PREFIX + version + LocalBufferFile.BUFFER_FILE_EXTENSION);
}
@Override
public File getChangeDataFile(int version) {
return new File(dbDir,
CHANGE_FILE_PREFIX + version + LocalBufferFile.BUFFER_FILE_EXTENSION);
}
@Override
public File getChangeMapFile() {
return null;
}
@Override
public void versionCreated(int version, String comment, long checkinId)
throws FileNotFoundException {
synchronized (syncObject) {
File bfile = getBufferFile(version);
long createTime = bfile.lastModified();
if (createTime == 0) {
log.error(dbDir + ": new version not found (" + version + ")");
return;
}
if (currentVersion != (version - 1)) {
log.error(dbDir + ": unexpected version created (" + version +
"), expected version " + (currentVersion + 1));
if (version > currentVersion || version < minVersion) {
bfile.delete();
}
return;
}
if (!verDBListener.versionCreated(VersionedDatabase.this, version, createTime,
comment, checkinId)) {
bfile.delete();
if (!bfile.exists()) {
log.info(dbDir + ": version " + version + " removed");
version = currentVersion;
}
}
scanFiles(true);
if (currentVersion == 0) {
throw new FileNotFoundException("Database files not found");
}
if (version != currentVersion) {
log.error(dbDir + ": Unexpected version found (" + currentVersion +
"), expected " + version);
}
}
}
@Override
public void updateEnded(long checkinId) {
synchronized (syncObject) {
verDBListener.checkinCompleted(checkinId);
}
}
}
}

View file

@ -0,0 +1,17 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body>
<p>
Extends the general Database implementation in support of LocalDatabaseItem's. Support is provided
for three types of Databases:
</p>
<ol>
<li>PrivateDatabase (non-versioned)</li>
<li>VersionedDatabase</li>
<li>PackedDatabase</li>
</ol>
</body>
</html>

View file

@ -0,0 +1,388 @@
/* ###
* 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.store.local;
import java.io.*;
import java.util.*;
import org.jdom.*;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import ghidra.framework.store.*;
import ghidra.util.datastruct.LongObjectHashtable;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
/**
* <code>CheckoutManager</code> manages checkout data for a versioned
* LocalFolderItem. Checkout data is maintained within the file 'checkout.dat'
* located within the items data directory.
*/
class CheckoutManager {
static final String CHECKOUTS_FILE = "checkout.dat";
private LocalFolderItem item;
private long nextCheckoutId = 1;
// checkouts maps long checkoutId to ItemCheckoutStatus objects
private LongObjectHashtable<ItemCheckoutStatus> checkouts;
/**
* Constructor.
*
* @param item folder item
* @param create if true an empty checkout data file is written, else the
* initial data is read from the file.
* @throws IOException
*/
CheckoutManager(LocalFolderItem item, boolean create) throws IOException {
this.item = item;
if (create) {
checkouts = new LongObjectHashtable<ItemCheckoutStatus>();
writeCheckoutsFile();
}
}
/**
* Returns the file which contains checkout data.
*/
private File getCheckoutsFile() {
return new File(item.getDataDir(), CHECKOUTS_FILE);
}
/**
* Requests a new checkout for the associated item.
*
* @param checkoutType type of checkout
* @param user name of user requesting checkout
* @param version item version to be checked-out
* @return checkout data or null if exclusive checkout denied due to
* existing checkouts.
* @throws IOException if checkout fails
*/
synchronized ItemCheckoutStatus newCheckout(CheckoutType checkoutType, String user, int version,
String projectPath) throws IOException {
validate();
if (checkoutType == null) {
throw new IllegalArgumentException("checkoutType must be specified");
}
ItemCheckoutStatus[] coList = getAllCheckouts();
if (coList.length != 0) {
if (checkoutType != CheckoutType.NORMAL) {
return null;
}
if (coList[0].getCheckoutType() == CheckoutType.TRANSIENT) {
throw new ExclusiveCheckoutException(
"File temporarily checked out exclusively by: " + coList[0].getUser());
}
if (coList[0].getCheckoutType() == CheckoutType.EXCLUSIVE) {
throw new ExclusiveCheckoutException(
"File checked out exclusively to another project by: " + coList[0].getUser());
}
}
ItemCheckoutStatus coStatus = new ItemCheckoutStatus(nextCheckoutId++, checkoutType, user,
version, (new Date()).getTime(), projectPath);
checkouts.put(coStatus.getCheckoutId(), coStatus);
if (checkoutType != CheckoutType.TRANSIENT) {
writeCheckoutsFile();
}
item.log("checkout (" + coStatus.getCheckoutId() + ") granted", user);
return coStatus;
}
/**
* Update the version associated with the specified checkout
*
* @param checkoutId checkout ID to be updated
* @param version item version to be associated with checkout
*/
synchronized void updateCheckout(long checkoutId, int version) throws IOException {
validate();
ItemCheckoutStatus coStatus = checkouts.remove(checkoutId);
if (coStatus != null) {
CheckoutType checkoutType = coStatus.getCheckoutType();
coStatus = new ItemCheckoutStatus(checkoutId, checkoutType, coStatus.getUser(), version,
(new Date()).getTime(), coStatus.getProjectPath());
checkouts.put(checkoutId, coStatus);
if (checkoutType != CheckoutType.TRANSIENT) {
try {
writeCheckoutsFile();
}
catch (IOException e) {
item.log("ERROR! failed to update checkout version", coStatus.getUser());
}
}
}
}
/**
* Terminate the specified checkout
*
* @param checkoutId checkout ID
* @throws IOException
*/
synchronized void endCheckout(long checkoutId) throws IOException {
validate();
ItemCheckoutStatus coStatus = checkouts.remove(checkoutId);
if (coStatus != null) {
item.log("checkout (" + checkoutId + ") ended", coStatus.getUser());
if (coStatus.getCheckoutType() != CheckoutType.TRANSIENT) {
boolean success = false;
try {
writeCheckoutsFile();
success = true;
}
finally {
if (!success) {
checkouts.put(checkoutId, coStatus);
}
}
}
}
}
/**
* Returns true if the specified version of the associated item is
* checked-out.
*
* @param version the specific version to check for checkouts.
*/
synchronized boolean isCheckedOut(int version) throws IOException {
validate();
long[] ids = checkouts.getKeys();
for (long id : ids) {
ItemCheckoutStatus coStatus = checkouts.get(id);
if (coStatus.getCheckoutVersion() == version) {
return true;
}
}
return false;
}
/**
* Returns true if the any version of the associated item is checked-out.
*/
synchronized boolean isCheckedOut() throws IOException {
validate();
return checkouts.size() != 0;
}
/**
* Returns the checkout data corresponding to the specified checkout ID.
* Null is returned if checkout ID is not found.
*
* @param checkoutId checkout ID
*/
synchronized ItemCheckoutStatus getCheckout(long checkoutId) throws IOException {
validate();
return checkouts.get(checkoutId);
}
/**
* Returns the checkout data for all existing checkouts of the associated
* item.
*/
synchronized ItemCheckoutStatus[] getAllCheckouts() throws IOException {
validate();
long[] ids = checkouts.getKeys();
Arrays.sort(ids);
ItemCheckoutStatus[] list = new ItemCheckoutStatus[ids.length];
for (int i = 0; i < ids.length; i++) {
list[i] = checkouts.get(ids[i]);
}
return list;
}
/**
* If validationRequired is true and the checkout data file has been
* updated, the checkout data will be re-initialized from the file. This is
* undesirable and is only required when multiple instances of a
* LocalFolderItem are used for a specific item path (e.g., unit testing).
*/
private void validate() throws IOException {
if (LocalFileSystem.isRefreshRequired()) {
checkouts = null;
}
if (checkouts == null) {
LongObjectHashtable<ItemCheckoutStatus> oldCheckouts = checkouts;
long oldNextCheckoutId = nextCheckoutId;
boolean success = false;
try {
readCheckoutsFile();
success = true;
}
finally {
if (!success) {
nextCheckoutId = oldNextCheckoutId;
checkouts = oldCheckouts;
}
}
}
}
/**
* Read data from checkout file.
*
* @throws IOException
*/
@SuppressWarnings("unchecked")
private void readCheckoutsFile() throws IOException {
checkouts = new LongObjectHashtable<ItemCheckoutStatus>();
File checkoutsFile = getCheckoutsFile();
if (!checkoutsFile.exists()) {
return;
}
FileInputStream istream = new FileInputStream(checkoutsFile);
BufferedInputStream bis = new BufferedInputStream(istream);
try {
SAXBuilder sax = XmlUtilities.createSecureSAXBuilder(false, false);
Document doc = sax.build(bis);
Element root = doc.getRootElement();
String nextId = root.getAttributeValue("NEXT_ID");
try {
nextCheckoutId = Long.parseLong(nextId);
}
catch (NumberFormatException e) {
throw new IOException("Invalid checkouts file: " + checkoutsFile);
}
List<Element> elementList = root.getChildren("CHECKOUT");
Iterator<Element> iter = elementList.iterator();
while (iter.hasNext()) {
ItemCheckoutStatus coStatus = parseCheckoutElement(iter.next());
checkouts.put(coStatus.getCheckoutId(), coStatus);
}
}
catch (org.jdom.JDOMException je) {
throw new InvalidObjectException("Invalid checkouts file: " + checkoutsFile);
}
finally {
istream.close();
}
}
/**
* Parse checkout element from file.
*
* @param coElement checkout data element
* @return checkout data for specified element
* @throws JDOMException
*/
ItemCheckoutStatus parseCheckoutElement(Element coElement) throws JDOMException {
try {
long checkoutId = Long.parseLong(coElement.getAttributeValue("ID"));
String user = coElement.getAttributeValue("USER");
int checkoutVersion = Integer.parseInt(coElement.getAttributeValue("VERSION"));
long time = Long.parseLong(coElement.getAttributeValue("TIME"));
String projectPath = coElement.getAttributeValue("PROJECT");
String val = coElement.getAttributeValue("EXCLUSIVE");
boolean exclusive = val != null ? Boolean.valueOf(val).booleanValue() : false;
CheckoutType checkoutType = exclusive ? CheckoutType.EXCLUSIVE : CheckoutType.NORMAL;
return new ItemCheckoutStatus(checkoutId, checkoutType, user, checkoutVersion, time,
projectPath);
}
catch (NumberFormatException e) {
throw new JDOMException("Bad CHECKOUT element");
}
}
/**
* Write checkout data file.
*
* @throws IOException
*/
private void writeCheckoutsFile() throws IOException {
// Output checkouts as XML
Element root = new Element("CHECKOUT_LIST");
root.setAttribute("NEXT_ID", Long.toString(nextCheckoutId));
long[] ids = checkouts.getKeys();
for (long id : ids) {
ItemCheckoutStatus coStatus = checkouts.get(id);
// TRANSIENT checkout data must not be persisted - the existence
// of such checkouts is retained in-memory only
if (coStatus.getCheckoutType() != CheckoutType.TRANSIENT) {
root.addContent(getCheckoutElement(coStatus));
}
}
File checkoutsFile = getCheckoutsFile();
// Store checkout data in temporary file
File tmpFile = new File(checkoutsFile.getParentFile(), checkoutsFile.getName() + ".new");
tmpFile.delete();
FileOutputStream ostream = new FileOutputStream(tmpFile);
BufferedOutputStream bos = new BufferedOutputStream(ostream);
try {
Document doc = new Document(root);
XMLOutputter xmlout = new GenericXMLOutputter();
xmlout.output(doc, bos);
}
finally {
bos.close();
}
// Rename files
File oldFile = null;
if (checkoutsFile.exists()) {
oldFile = new File(checkoutsFile.getParentFile(), checkoutsFile.getName() + ".bak");
oldFile.delete();
if (!checkoutsFile.renameTo(oldFile)) {
throw new IOException("Failed to update checkouts: " + item.getPathName());
}
}
if (!tmpFile.renameTo(checkoutsFile)) {
if (oldFile != null) {
oldFile.renameTo(checkoutsFile);
}
throw new IOException("Failed to update checkouts: " + item.getPathName());
}
if (oldFile != null) {
oldFile.delete();
}
}
/**
* Build checkout data element
*
* @param coStatus checkout data
* @return checkout data element
*/
Element getCheckoutElement(ItemCheckoutStatus coStatus) {
Element element = new Element("CHECKOUT");
element.setAttribute("ID", Long.toString(coStatus.getCheckoutId()));
element.setAttribute("USER", coStatus.getUser());
element.setAttribute("VERSION", Integer.toString(coStatus.getCheckoutVersion()));
element.setAttribute("TIME", Long.toString(coStatus.getCheckoutTime()));
String projectPath = coStatus.getProjectPath();
if (projectPath != null) {
element.setAttribute("PROJECT", projectPath);
}
element.setAttribute("EXCLUSIVE",
Boolean.toString(coStatus.getCheckoutType() == CheckoutType.EXCLUSIVE));
return element;
}
}

View file

@ -0,0 +1,47 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import java.io.File;
import java.io.IOException;
/**
* <code>DataDirectoryException</code> is thrown when a folder item can not be
* created because its associated data directory already exists.
*/
public class DataDirectoryException extends IOException {
private File dir;
/**
* Constructor.
* @param msg error message
* @param dir existing data directory
*/
public DataDirectoryException(String msg, File dir) {
super(msg);
this.dir = dir;
}
/**
* Returns existing data directory
*/
public File getDataDirectory() {
return dir;
}
}

View file

@ -0,0 +1,48 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import java.io.File;
/**
*
* Defines a file change listener interface.
*
*/
public interface FileChangeListener {
/**
* Used to notify a listener that the specified file has been modified.
* If the file watcher was created with a lock file, the lock will be set
* on behalf of the caller. This method should not attempt to alter the
* lock.
* @param file the modified file.
* @param haveLock is true if a file lock has been granted (LockFile was
* supplied at time of construction).
*/
public void fileModified(File file);
/**
* Used to notify a listener that the specified file has been removed.
* If the file watcher was created with a lock file, the lock will be set
* on behalf of the caller. This method should not attempt to alter the
* lock.
* @param file the removed file.
*/
public void fileRemoved(File file);
}

View file

@ -0,0 +1,453 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.framework.store.Version;
import java.io.*;
import java.util.*;
/**
* <code>HistoryManager</code> manages version data for a versioned LocalFolderItem.
* History data is maintained within the file 'historyt.dat' located within the
* items data directory.
*/
class HistoryManager {
static final String HISTORY_FILE = "history.dat";
private LocalFolderItem item;
private int minVersion;
private int curVersion;
private Version[] versions;
/**
* Constructor.
* @param item folder item
* @param create if true an empty history data file is written,
* else the initial data is read from the file.
* @throws IOException
*/
HistoryManager(LocalFolderItem item, boolean create) throws IOException {
this.item = item;
if (create) {
versions = new Version[0];
}
}
/**
* Returns the file which contains version history data. Content of this
* file is managed by the HistoryManager.
*/
private File getHistoryFile() {
return new File(item.getDataDir(), HISTORY_FILE);
}
/**
* Add and/or remove history entries to agree with specified minimum and current versions.
* @param minVersion minimum version
* @param curVersion current version
* @return true if a version correction was performed
*/
synchronized boolean fixHistory(int minVersion, int curVersion) throws IOException {
validate();
if (minVersion == this.minVersion && curVersion == this.curVersion) {
return false;
}
if (minVersion < 1 || curVersion < minVersion) {
throw new IllegalArgumentException();
}
Version[] newVersions = new Version[curVersion - minVersion + 1];
int newIx = 0;
int oldIx = 0;
int version = minVersion;
if (minVersion < this.minVersion) {
while (version < this.minVersion && version <= curVersion) {
// Add missing versions
newVersions[newIx++] = new Version(version++, 0, "<Unknown>", "<Recovered>");
}
}
if (version >= this.minVersion && version <= this.curVersion) {
// keep as many existing version entries as possible
while (versions[oldIx].getVersion() < version) {
++oldIx;
}
while (version <= this.curVersion && version <= curVersion) {
newVersions[newIx++] = versions[oldIx++];
++version;
}
}
while (version <= curVersion) {
// Add missing versions
newVersions[newIx++] = new Version(version++, 0, "<Unknown>", "<Recovered>");
}
versions = newVersions;
this.minVersion = minVersion;
this.curVersion = curVersion;
writeHistoryFile();
return true;
}
/**
* Record the creation of a new item version.
* @param version version number
* @param user user who created version
*/
synchronized void versionAdded(int version, long time, String comment, String user)
throws IOException {
validate();
// Validate version
if (version != curVersion + 1) {
// Check should have been performed by item
item.log("ERROR! unexpected version " + version + " created, expected version " +
(curVersion + 1), user);
return;
}
item.log("version " + version + " created", user);
Version ver = new Version(version, time, user, comment);
appendHistoryFile(ver);
Version[] newVersions = new Version[versions.length + 1];
System.arraycopy(versions, 0, newVersions, 0, versions.length);
newVersions[versions.length] = ver;
versions = newVersions;
curVersion = version;
if (version == 1) {
minVersion = 1;
}
}
/**
* Remove the specified version from the history data.
* This method only modifies the data if the minimum or
* latest version is specified.
* @param version minimum or latest version
*/
synchronized void versionDeleted(int version, String user) throws IOException {
validate();
if (versions.length <= 1) {
// Check should have been performed by item - item should be deleted instead
item.log("ERROR! version " + version + " deleted illegally, min=" + minVersion +
", max=" + curVersion, user);
return;
}
Version[] newVersions = new Version[versions.length - 1];
if (version == versions[0].getVersion()) {
System.arraycopy(versions, 1, newVersions, 0, versions.length - 1);
minVersion = newVersions[0].getVersion();
}
else if (version == versions[versions.length - 1].getVersion()) {
System.arraycopy(versions, 0, newVersions, 0, versions.length - 1);
curVersion = newVersions[newVersions.length - 1].getVersion();
}
else {
// Check should have been performed by item
item.log("ERROR! version " + version + " deleted illegally, min=" + minVersion +
", max=" + curVersion, user);
return;
}
item.log("version " + version + " deleted", user);
versions = newVersions;
writeHistoryFile();
}
// int getMinimumVersion() {
// validate();
// return minVersion;
// }
//
// int getCurrentVersion() {
// validate();
// return curVersion;
// }
/**
* Return all versions contained within the history. Versions are
* ordered oldest to newest (i.e., minumum to latest).
* @throws IOException if an IO error occurs.
*/
synchronized Version[] getVersions() throws IOException {
validate();
return versions.clone();
}
/**
* Return specific version.
* @param version item version
* @return version object or null if not found
* @throws IOException if an IO error occurs.
*/
synchronized Version getVersion(int version) throws IOException {
validate();
if (version >= minVersion && version < curVersion) {
return versions[version - minVersion];
}
return null;
}
/**
* If validationRequired is true and the history data file has been
* updated, the history data will be re-initialized from the file.
* This is undesirable and is only required when mulitple instances
* of a LocalFolderItem are used for a specific item path (e.g., unit testing).
*/
private void validate() throws IOException {
if (LocalFileSystem.isRefreshRequired()) {
versions = null;
minVersion = 0;
curVersion = 0;
}
File historyFile = getHistoryFile();
if (historyFile.exists()) {
Version[] oldVersions = versions;
int oldMinVersion = minVersion;
int oldCurVersion = curVersion;
boolean success = false;
try {
readHistoryFile();
success = true;
}
finally {
if (!success) {
versions = oldVersions;
minVersion = oldMinVersion;
curVersion = oldCurVersion;
}
}
}
else {
versions = new Version[0];
}
}
/**
* Read data from history file.
* @throws IOException
*/
private void readHistoryFile() throws IOException {
ArrayList<Version> list = new ArrayList<Version>();
minVersion = 0;
curVersion = 0;
File historyFile = getHistoryFile();
BufferedReader in = new BufferedReader(new FileReader(historyFile));
try {
String line = in.readLine();
while (line != null) {
Version ver;
try {
ver = decodeVersion(line);
}
catch (Exception e) {
throw new IOException("Bad history file: " + historyFile);
}
int version = ver.getVersion();
if (curVersion != 0 && version != (curVersion + 1)) {
// Versions must be in sequential order
throw new IOException("Bad history file" + historyFile);
}
if (minVersion == 0) {
minVersion = version;
}
curVersion = version;
list.add(ver);
line = in.readLine();
}
}
finally {
in.close();
}
versions = new Version[list.size()];
list.toArray(versions);
}
/**
* Write all history data to file.
*/
private void writeHistoryFile() {
File historyFile = getHistoryFile();
try {
File tmpFile = new File(historyFile.getParentFile(), historyFile.getName() + ".new");
tmpFile.delete();
BufferedWriter out = new BufferedWriter(new FileWriter(tmpFile));
for (int i = 0; i < versions.length; i++) {
out.write(encodeVersion(versions[i]));
out.newLine();
}
out.close();
// Rename files
File oldFile = null;
if (historyFile.exists()) {
oldFile = new File(historyFile.getParentFile(), historyFile.getName() + ".bak");
oldFile.delete();
if (!historyFile.renameTo(oldFile)) {
throw new IOException("file is in use");
}
}
if (!tmpFile.renameTo(historyFile)) {
if (oldFile != null) {
oldFile.renameTo(historyFile);
}
throw new IOException("file error - backup may exist");
}
if (oldFile != null) {
oldFile.delete();
}
}
catch (IOException e) {
item.log("ERROR! failed to update history file: " + e.toString(), null);
}
}
/**
* Write new version data to file.
* @param ver new version data (must be latest version)
*/
private void appendHistoryFile(Version ver) {
File historyFile = getHistoryFile();
try {
BufferedWriter out = new BufferedWriter(new FileWriter(historyFile, true));
out.write(encodeVersion(ver));
out.newLine();
out.close();
}
catch (IOException e) {
item.log("ERROR! failed to update history file: " + e.toString(), null);
}
}
/**
* Encode item version data for file output.
* @param ver version data
* @return
*/
private String encodeVersion(Version ver) {
StringBuffer buf = new StringBuffer();
buf.append(ver.getVersion());
buf.append(';');
buf.append(ver.getUser());
buf.append(';');
buf.append(ver.getCreateTime());
buf.append(';');
encodeString(ver.getComment(), buf);
return buf.toString();
}
/**
* Decode item version data from file.
* @param line file input line
* @return parsed version data
* @throws NumberFormatException
* @throws NoSuchElementException
*/
private Version decodeVersion(String line) throws NumberFormatException, NoSuchElementException {
StringTokenizer st = new StringTokenizer(line, ";");
int version = Integer.parseInt(st.nextToken());
String user = st.nextToken();
long time = Long.parseLong(st.nextToken());
String comment = "";
if (st.hasMoreTokens()) {
comment = decodeString(st.nextToken());
}
return new Version(version, time, user, comment);
}
/**
* Escape special characters within a string and output to string buffer.
* @param text text string to be escaped
* @param buf output buffer
*/
private void encodeString(String text, StringBuffer buf) {
if (text == null) {
return;
}
for (int i = 0; i < text.length(); i++) {
char next = text.charAt(i);
switch (next) {
case '\n':
buf.append("\\n");
break;
case '\r':
buf.append("\\r");
break;
case ';':
buf.append("\\s");
break;
case '\\':
buf.append("\\\\");
break;
default:
buf.append(next);
}
}
}
/**
* Decode an escaped string.
* @param text string containing escaped characters.
* @return decoded string
*/
private String decodeString(String text) {
if (text == null) {
return "";
}
StringBuffer buf = new StringBuffer();
boolean controlChar = false;
for (int i = 0; i < text.length(); i++) {
char next = text.charAt(i);
if (next == '\\') {
controlChar = true;
}
else if (controlChar) {
switch (next) {
case 'n':
buf.append('\n');
break;
case 'r':
buf.append('\r');
break;
case 's':
buf.append(';');
break;
default:
buf.append(next);
}
controlChar = false;
}
else {
buf.append(next);
}
}
return buf.toString();
}
}

View file

@ -0,0 +1,108 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.framework.store.FileSystem;
import ghidra.util.PropertyFile;
import ghidra.util.exception.DuplicateFileException;
import java.io.*;
public class IndexedPropertyFile extends PropertyFile {
public final static String NAME_PROPERTY = "NAME";
public final static String PARENT_PATH_PROPERTY = "PARENT";
/**
* Construct a new or existing PropertyFile.
* This form ignores retained property values for NAME and PARENT path.
* @param dir parent directory
* @param storageName stored property file name (without extension)
* @param parentPath path to parent
* @param name name of the property file
* @throws IOException
*/
public IndexedPropertyFile(File dir, String storageName, String parentPath, String name)
throws IOException {
super(dir, storageName, parentPath, name);
// if (exists() &&
// (!name.equals(getString(NAME_PROPERTY, null)) || !parentPath.equals(getString(
// PARENT_PATH_PROPERTY, null)))) {
// throw new AssertException();
// }
putString(NAME_PROPERTY, name);
putString(PARENT_PATH_PROPERTY, parentPath);
}
/**
* Construct an existing PropertyFile.
* @param dir parent directory
* @param storageName stored property file name (without extension)
* @throws FileNotFoundException if property file does not exist
* @throws IOException if error occurs reading property file
*/
public IndexedPropertyFile(File dir, String storageName) throws IOException {
super(dir, storageName, FileSystem.SEPARATOR, storageName);
if (!exists()) {
throw new FileNotFoundException();
}
if (name == null || parentPath == null) {
throw new IOException("Invalid indexed property file: " + propertyFile);
}
}
/**
* Construct an existing PropertyFile.
* @param file
* @throws FileNotFoundException if property file does not exist
* @throws IOException if error occurs reading property file
*/
public IndexedPropertyFile(File file) throws IOException {
this(file.getParentFile(), getStorageName(file.getName()));
}
private static String getStorageName(String propertyFileName) {
if (!propertyFileName.endsWith(PROPERTY_EXT)) {
throw new IllegalArgumentException("property file name must have .prp file extension");
}
return propertyFileName.substring(0, propertyFileName.length() - PROPERTY_EXT.length());
}
@Override
public void readState() throws IOException {
super.readState();
name = getString(NAME_PROPERTY, null);
parentPath = getString(PARENT_PATH_PROPERTY, null);
}
@Override
public void moveTo(File newParent, String newStorageName, String newParentPath, String newName)
throws DuplicateFileException, IOException {
super.moveTo(newParent, newStorageName, newParentPath, newName);
// if (!parentPath.equals(newParentPath)) {
// throw new AssertException();
// }
// if (!name.equals(newName)) {
// throw new AssertException();
// }
putString(NAME_PROPERTY, newName);
putString(PARENT_PATH_PROPERTY, newParentPath);
writeState();
}
}

View file

@ -0,0 +1,216 @@
/* ###
* 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.store.local;
import java.io.*;
import java.util.HashMap;
import ghidra.framework.store.FolderItem;
import ghidra.util.Msg;
import ghidra.util.PropertyFile;
import ghidra.util.exception.NotFoundException;
/**
* <code>IndexedLocalFileSystem</code> implements a case-sensitive indexed filesystem
* which uses a shallow storage hierarchy with no restriction on file name or path
* length. This filesystem is identified by the existence of an index file (~index.dat)
* and recovery journal (~index.jrn).
*/
public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
public static final int INDEX_VERSION = IndexedLocalFileSystem.LATEST_INDEX_VERSION; // 1
private HashMap<String, Item> fileIdMap;
/**
* Constructor.
* @param file path path for root directory.
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws FileNotFoundException if specified rootPath does not exist
* @throws IOException if error occurs while reading/writing index files
*/
IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
boolean enableAsyncronousDispatching, boolean create) throws IOException {
super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create);
}
/**
* Construct existing indexed filesystem with an empty index.
* This can be used to prepare for rebuilding the filesystem index.
* @param rootPath
* @throws IOException
*/
private IndexedV1LocalFileSystem(String rootPath) throws IOException {
super(rootPath);
}
@Override
public int getIndexImplementationVersion() {
return INDEX_VERSION;
}
@Override
String formatIndexItem(Item item) {
String entry = item.getStorageName() + INDEX_ITEM_SEPARATOR + item.getName();
String fileId = item.getFileID();
if (fileId != null) {
entry += INDEX_ITEM_SEPARATOR + fileId;
}
return entry;
}
@Override
Item parseIndexItem(Folder parent, String entry) {
int index = entry.indexOf(INDEX_ITEM_SEPARATOR);
if (index < 0) {
return null;
}
String storageName = entry.substring(0, index);
String name = entry.substring(index + 1);
String fileId = null;
index = name.indexOf(INDEX_ITEM_SEPARATOR);
if (index > 0) {
fileId = name.substring(index + 1);
name = name.substring(0, index);
}
return new Item(parent, name, fileId, storageName);
}
@Override
protected synchronized void fileIdChanged(PropertyFile pfile, String oldFileId)
throws IOException {
indexJournal.open();
try {
Folder folder = getFolder(pfile.getParentPath(), GetFolderOption.READ_ONLY);
Item item = folder.items.get(pfile.getName());
if (item == null) {
throw new NotFoundException(pfile.getPath());
}
item.setFileID(pfile.getFileID());
indexJournal.fileIdSet(pfile.getPath(), pfile.getFileID());
}
catch (NotFoundException e) {
throw new FileNotFoundException(e.getMessage());
}
finally {
indexJournal.close();
}
}
private HashMap<String, Item> getFileIdMap() {
if (fileIdMap == null) {
fileIdMap = new HashMap<>();
}
return fileIdMap;
}
@Override
void mapFileID(String fileId, Item item) {
getFileIdMap().put(fileId, item);
}
@Override
void unmapFileID(String fileId) {
getFileIdMap().remove(fileId);
}
@Override
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
if (fileIdMap == null) {
return null;
}
Item item = fileIdMap.get(fileID);
if (item == null) {
return null;
}
try {
PropertyFile propertyFile = item.itemStorage.getPropertyFile();
if (propertyFile.exists()) {
return LocalFolderItem.getFolderItem(this, propertyFile);
}
}
catch (FileNotFoundException e) {
// ignore
}
return null;
}
/**
* Get the V0 indexed-file-system instance. File system storage should first be
* pre-qualified as an having indexed storage using the {@link #isIndexed(String)} method
* and have the correct version.
* @param rootPath
* @param isVersioned
* @param readOnly
* @param enableAsyncronousDispatching
* @return file-system instance
* @throws IOException
*/
static IndexedV1LocalFileSystem getFileSystem(String rootPath, boolean isVersioned,
boolean readOnly, boolean enableAsyncronousDispatching) throws IOException {
try {
return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, false);
}
catch (IndexReadException e) {
if (readOnly) {
throw e; // don't attempt repair if read-only
}
Msg.error(LocalFileSystem.class, "Indexed filesystem error: " + e.getMessage());
Msg.info(LocalFileSystem.class, "Attempting index rebuild: " + rootPath);
if (!IndexedV1LocalFileSystem.rebuild(new File(rootPath))) {
throw e;
}
// retry after index rebuild
return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, false);
}
}
/**
* Completely rebuild filesystem index using item information contained
* within indexed property files. Empty folders will be lost.
* @param rootDir
* @throws IOException
*/
public static boolean rebuild(File rootDir) throws IOException {
verifyIndexedFileStructure(rootDir);
IndexedV1LocalFileSystem fs = new IndexedV1LocalFileSystem(rootDir.getAbsolutePath());
fs.rebuildIndex();
fs.cleanupAfterConstruction();
fs.dispose();
File errorFile = new File(rootDir, REBUILD_ERROR_FILE);
if (errorFile.exists()) {
Msg.error(LocalFileSystem.class,
"Indexed filesystem rebuild failed, see log for details: " + errorFile);
return false;
}
Msg.info(LocalFileSystem.class, "Index rebuild completed: " + rootDir);
return true;
}
}

View file

@ -0,0 +1,195 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import generic.jar.ResourceFile;
import ghidra.util.MonitoredInputStream;
import ghidra.util.exception.IOCancelledException;
import ghidra.util.task.TaskMonitor;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* <code>ItemDeserializer</code> facilitates the reading of a compressed data stream
* contained within a "packed" file. A "packed" file contains the following meta-data
* which is available after construction:
* <ul>
* <li>Item name</li>
* <li>Content type (int)</li>
* <li>File type (int)</li>
* <li>Data length</li>
* </ul>
*/
public class ItemDeserializer {
private static final long MAGIC_NUMBER = ItemSerializer.MAGIC_NUMBER;
private static final int FORMAT_VERSION = ItemSerializer.FORMAT_VERSION;
private static final String ZIP_ENTRY_NAME = ItemSerializer.ZIP_ENTRY_NAME;
private final static int IO_BUFFER_SIZE = ItemSerializer.IO_BUFFER_SIZE;
private InputStream in;
private String itemName;
private String contentType;
private int fileType;
private long length;
private boolean saved = false;
/**
* Constructor.
* @param in input stream. The input stream must not be read again until
* after the saveItem method has been invoked successfully.
* @throws IOException
*/
public ItemDeserializer(File packedFile) throws IOException {
this(new ResourceFile(packedFile));
}
public ItemDeserializer(ResourceFile packedFile) throws IOException {
in = new BufferedInputStream(packedFile.getInputStream());
// Read header containing: original item name and content type
boolean success = false;
try {
ObjectInputStream objIn = new ObjectInputStream(in);
if (objIn.readLong() != MAGIC_NUMBER) {
throw new IOException("Invalid data");
}
if (objIn.readInt() != FORMAT_VERSION) {
throw new IOException("Unsupported data format");
}
itemName = objIn.readUTF();
contentType = objIn.readUTF();
if (contentType.length() == 0) {
contentType = null;
}
fileType = objIn.readInt();
length = objIn.readLong();
success = true;
}
catch (UTFDataFormatException e) {
throw new IOException("Invalid item data");
}
finally {
if (!success) {
try {
in.close();
}
catch (IOException e) {
}
}
}
}
@Override
protected void finalize() throws Throwable {
dispose();
super.finalize();
}
/**
* Close packed-file input stream and free resources.
*/
public void dispose() {
if (in != null) {
try {
in.close();
}
catch (IOException e) {
}
finally {
in = null;
}
}
}
/**
* Returns packed item name
*/
public String getItemName() {
return itemName;
}
/**
* Returns packed content type
*/
public String getContentType() {
return contentType;
}
/**
* Returns packed file type.
*/
public int getFileType() {
return fileType;
}
/**
* Returns unpacked data length
*/
public long getLength() {
return length;
}
/**
* Save the item to the specified output stream.
* This method may only be invoked once.
* @param out
* @param monitor
* @throws IOException
*/
public void saveItem(OutputStream out, TaskMonitor monitor) throws IOCancelledException,
IOException {
if (saved) {
throw new IllegalStateException("Already saved");
}
saved = true;
ZipInputStream zipIn = new ZipInputStream(in);
ZipEntry entry = zipIn.getNextEntry();
if (entry == null || !ZIP_ENTRY_NAME.equals(entry.getName())) {
throw new IOException("Data error");
}
// if (length != entry.getSize()) {
// throw new IOException("Content length is " + entry.getSize() + ", expected " + length);
// }
InputStream itemIn = zipIn;
if (monitor != null) {
itemIn = new MonitoredInputStream(zipIn, monitor);
monitor.initialize((int) length);
}
long len = length;
byte[] buffer = new byte[IO_BUFFER_SIZE];
// Copy file contents
int cnt = (int) (len < IO_BUFFER_SIZE ? len : IO_BUFFER_SIZE);
while ((cnt = itemIn.read(buffer, 0, cnt)) > 0) {
out.write(buffer, 0, cnt);
len -= cnt;
cnt = (int) (len < IO_BUFFER_SIZE ? len : IO_BUFFER_SIZE);
}
}
}

View file

@ -0,0 +1,175 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.util.BigEndianDataConverter;
import ghidra.util.MonitoredOutputStream;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.IOCancelledException;
import ghidra.util.task.TaskMonitor;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* <code>ItemSerializer</code> facilitates the compressing and writing of a data stream
* to a "packed" file. The resulting "packed" file will contain the following meta-data
* which is available after construction:
* <ul>
* <li>Item name</li>
* <li>Content type (int)</li>
* <li>File type (int)</li>
* <li>Data length</li>
* </ul>
*/
public class ItemSerializer {
private static final int MAGIC_NUMBER_POS = 6;
private static final int MAGIC_NUMBER_SIZE = 8;
static final long MAGIC_NUMBER = 0x2e30212634e92c20L;
static final int FORMAT_VERSION = 1;
static final String ZIP_ENTRY_NAME = "FOLDER_ITEM";
static final int IO_BUFFER_SIZE = 32 * 1024;
private ItemSerializer() {
}
/**
* Read and compress data from the specified content stream and write to
* a packed file along with additional meta-data.
* @param itemName item name
* @param contentType content type
* @param fileType file type
* @param length content length to be read
* @param content content input stream
* @param packedFile output packed file to be created
* @param monitor task monitor
* @throws CancelledException
* @throws IOException
*/
public static void outputItem(String itemName, String contentType, int fileType, long length,
InputStream content, File packedFile, TaskMonitor monitor) throws CancelledException,
IOException {
OutputStream out = new BufferedOutputStream(new FileOutputStream(packedFile));
boolean success = false;
try {
// Output header containing: original item name and content type
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeLong(MAGIC_NUMBER);
objOut.writeInt(FORMAT_VERSION);
objOut.writeUTF(itemName);
objOut.writeUTF(contentType != null ? contentType : "");
objOut.writeInt(fileType);
objOut.writeLong(length);
objOut.flush();
// Output item content
ZipOutputStream zipOut = new ZipOutputStream(out);
ZipEntry entry = new ZipEntry(ZIP_ENTRY_NAME);
entry.setSize(length);
entry.setMethod(ZipEntry.DEFLATED);
zipOut.putNextEntry(entry);
OutputStream itemOut = zipOut;
if (monitor != null) {
itemOut = new MonitoredOutputStream(zipOut, monitor);
monitor.initialize((int) length);
}
long lengthWritten = 0;
byte[] buffer = new byte[IO_BUFFER_SIZE];
// Copy file contents
int cnt = 0;
while ((cnt = content.read(buffer)) > 0) {
itemOut.write(buffer, 0, cnt);
lengthWritten += cnt;
}
if (lengthWritten != length) {
throw new IOException("Did not write all content - written length is " +
lengthWritten + ", expected " + length + ".\n\tItem: " + itemName + " in " +
"packed file: " + packedFile.getAbsolutePath());
}
itemOut.flush();
zipOut.closeEntry();
zipOut.flush();
success = true;
}
catch (IOCancelledException e) {
throw new CancelledException();
}
finally {
try {
out.close();
if (!success) {
packedFile.delete();
}
}
catch (IOException e) {
// we tried
}
}
}
/**
* A simple utility method to determine if the given file is a packed file as created by
* this class.
* @param file The file to check
* @return True if it is a packed file
* @throws IOException If there is a problem reading the given file
*/
public static boolean isPackedFile(File file) throws IOException {
InputStream inputStream = null;
try {
inputStream = new BufferedInputStream(new FileInputStream(file));
return isPackedFile(inputStream);
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
// we tried
}
}
}
}
/**
* A convenience method for checking if the file denoted by the given inputStream is a
* packed file.
* <p>
* <b>Note: </b> This method will NOT close the given inputStream.
* @param inputStream a stream for accessing bytes of what may be a packed file
* @return true if the bytes from the inputStream represent the bytes of a packed file
* @throws IOException If there is a problem accessing the inputStream
* @see {@link #isPackedFile(File)}
*/
public static boolean isPackedFile(InputStream inputStream) throws IOException {
inputStream.skip(MAGIC_NUMBER_POS);
byte[] magicBytes = new byte[MAGIC_NUMBER_SIZE];
inputStream.read(magicBytes);
BigEndianDataConverter dc = new BigEndianDataConverter();
long magic = dc.getLong(magicBytes);
return (magic == MAGIC_NUMBER);
}
}

View file

@ -0,0 +1,220 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.framework.store.DataFileItem;
import ghidra.framework.store.FolderItem;
import ghidra.util.PropertyFile;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
import java.io.*;
/**
* <code>LocalDataFile</code> provides a FolderItem implementation
* for a local serialized data file. This implementation supports
* a non-versioned file-system only.
* <p>
* This item utilizes a data directory for storing the serialized
* data file.
*/
public class LocalDataFile extends LocalFolderItem implements DataFileItem {
private final static int IO_BUFFER_SIZE = 32 * 1024;
private static final String DATA_FILE = "data.1.gdf";
public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException {
super(fileSystem, propertyFile, true, false);
if (fileSystem.isVersioned()) {
throw new IOException("Item may be corrupt: " + getName());
}
if (!getDataFile().exists()) {
throw new FileNotFoundException(getName() + " not found");
}
}
/**
* Create a new local data file item.
* @param fileSystem file system
* @param propertyFile serialized data property file
* @param istream data source input stream (should be a start of data and will be read to end of file).
* The invoker of this constructor is responsible for closing istream.
* @param contentType user content type
* @param monitor progress monitor (used for cancel support,
* progress not used since length of input stream is unknown)
* @throws IOException if an IO Error occurs
* @throws CancelledException if monitor cancels operation
*/
public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile,
InputStream istream, String contentType, TaskMonitor monitor) throws IOException,
CancelledException {
super(fileSystem, propertyFile, true, true);
if (fileSystem.isVersioned()) {
abortCreate();
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
File dataFile = getDataFile();
if (dataFile.exists()) {
throw new DuplicateFileException(getName() + " already exists.");
}
propertyFile.putInt(FILE_TYPE, DATAFILE_FILE_TYPE);
propertyFile.putBoolean(READ_ONLY, false);
propertyFile.putString(CONTENT_TYPE, contentType);
propertyFile.writeState();
if (istream != null) {
boolean success = false;
byte[] buffer = new byte[IO_BUFFER_SIZE];
FileOutputStream out = new FileOutputStream(dataFile);
try {
int cnt = 0;
while ((cnt = istream.read(buffer)) >= 0) {
out.write(buffer, 0, cnt);
}
success = true;
}
finally {
try {
out.close();
}
catch (IOException e) {
}
if (!success) {
abortCreate();
}
}
}
else {
if (!dataFile.createNewFile()) {
abortCreate();
}
}
}
@Override
public long length() throws IOException {
return getDataFile().length();
}
/**
* Returns data File.
*/
private File getDataFile() {
return new File(getDataDir(), DATA_FILE);
}
/**
* @see ghidra.framework.store.DataFile#getInputStream()
*/
public InputStream getInputStream() throws FileNotFoundException {
return new FileInputStream(getDataFile());
}
/**
* @see ghidra.framework.store.DataFileItem#getInputStream(int)
*/
public InputStream getInputStream(int version) throws FileNotFoundException {
// TODO Versions for DataFiles are not supported
return new FileInputStream(getDataFile());
}
/**
* @see ghidra.framework.store.DataFile#getOutputStream()
*/
public OutputStream getOutputStream() throws FileNotFoundException {
return new FileOutputStream(getDataFile());
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, boolean, ghidra.util.task.TaskMonitor)
*/
@Override
public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, int)
*/
@Override
public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException {
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteMinimumVersion(java.lang.String)
*/
@Override
void deleteMinimumVersion(String user) throws IOException {
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteCurrentVersion(java.lang.String)
*/
@Override
void deleteCurrentVersion(String user) throws IOException {
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
/*
* @see ghidra.framework.store.FolderItem#output(java.io.File, int, ghidra.util.task.TaskMonitor)
*/
public void output(File outputFile, int version, TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException("Output not yet supported for DataFiles");
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#getMinimumVersion()
*/
@Override
int getMinimumVersion() throws IOException {
return -1;
}
/*
* @see ghidra.framework.store.FolderItem#getCurrentVersion()
*/
public int getCurrentVersion() {
return -1;
}
/*
* @see ghidra.framework.store.FolderItem#canRecover()
*/
public boolean canRecover() {
return false;
}
}

View file

@ -0,0 +1,120 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.framework.store.DataFileHandle;
import java.io.*;
/**
* <code>LocalDataFileHandle</code> provides random access to
* a local File.
*/
public class LocalDataFileHandle implements DataFileHandle {
private RandomAccessFile raf;
private boolean readOnly;
/**
* Construct and open a local DataFileHandle.
* @param file file to be opened
* @param readOnly if true resulting handle may only be read.
* @throws FileNotFoundException if file was not found
* @throws IOException if an IO Error occurs
*/
public LocalDataFileHandle(File file, boolean readOnly) throws IOException {
this.readOnly = readOnly;
raf = new RandomAccessFile(file, readOnly ? "r" : "rw");
}
/*
* @see ghidra.framework.store.DataFileHandle#read(byte[])
*/
public void read(byte[] b) throws IOException {
raf.readFully(b);
}
/*
* @see ghidra.framework.store.DataFileHandle#read(byte[], int, int)
*/
public void read(byte[] b, int off, int len) throws IOException {
raf.readFully(b, off, len);
}
/*
* @see ghidra.framework.store.DataFileHandle#skipBytes(int)
*/
public int skipBytes(int n) throws IOException {
return raf.skipBytes(n);
}
/*
* @see ghidra.framework.store.DataFileHandle#write(int)
*/
public void write(int b) throws IOException {
raf.write(b);
}
/*
* @see ghidra.framework.store.DataFileHandle#write(byte[])
*/
public void write(byte[] b) throws IOException {
raf.write(b);
}
/*
* @see ghidra.framework.store.DataFileHandle#write(byte[], int, int)
*/
public void write(byte[] b, int off, int len) throws IOException {
raf.write(b, off, len);
}
/*
* @see ghidra.framework.store.DataFileHandle#seek(long)
*/
public void seek(long pos) throws IOException {
raf.seek(pos);
}
/*
* @see ghidra.framework.store.DataFileHandle#length()
*/
public long length() throws IOException {
return raf.length();
}
/*
* @see ghidra.framework.store.DataFileHandle#setLength(long)
*/
public void setLength(long newLength) throws IOException {
raf.setLength(newLength);
}
/*
* @see ghidra.framework.store.DataFileHandle#close()
*/
public void close() throws IOException {
raf.close();
}
/*
* @see ghidra.framework.store.DataFileHandle#isReadOnly()
*/
public boolean isReadOnly() throws IOException {
return readOnly;
}
}

View file

@ -0,0 +1,740 @@
/* ###
* 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.store.local;
import java.io.File;
import java.io.IOException;
import db.buffers.*;
import ghidra.framework.store.*;
import ghidra.framework.store.db.*;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>LocalDatabaseItem</code> provides a FolderItem implementation
* for a local database. This item wraps an underlying VersionedDatabase
* if the file-system is versioned, otherwise a PrivateDatabase is wrapped.
* <p>
* This item utilizes a data directory for storing all files relating to the
* database as well as history and checkout data files if this item is versioned.
*/
public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
private PrivateDatabase privateDb;
private VersionedDatabase versionedDb;
private LocalVersionedDbListener versionedDbListener;
private String deleteUser;
/**
* Constructor for a new or existing local database item which corresponds to the specified
* property file.
* @param fileSystem file system
* @param propertyFile database property file
* @param create if true the data directory will be created
* @throws IOException
*/
private LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean create)
throws IOException {
super(fileSystem, propertyFile, true, create);
if (isVersioned) {
versionedDbListener = new LocalVersionedDbListener();
}
}
/**
* Constructor for an existing local database item which corresponds to the specified
* property file.
* @param fileSystem file system
* @param propertyFile database property file
*/
LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException {
super(fileSystem, propertyFile, true, false);
if (isVersioned) {
versionedDbListener = new LocalVersionedDbListener();
versionedDb = new VersionedDatabase(getDataDir(), versionedDbListener);
versionedDb.setSynchronizationObject(fileSystem);
}
else {
privateDb = new PrivateDatabase(getDataDir());
privateDb.setIsCheckoutCopy(isCheckedOut());
privateDb.setSynchronizationObject(fileSystem);
}
}
/**
* Create a new local Database item which corresponds to the specified
* property file. The initial contents of the database are copied from the
* specified srcFile.
* @param fileSystem file system
* @param propertyFile database property file
* @param srcFile open source Database buffer file
* @param contentType user content type
* @param fileID unique file ID
* @param comment if versioned, comment used for version 1 history data
* @param resetDatabaseId if true database ID will be reset for new Database
* @param monitor copy progress monitor
* @param user if versioned, user used for permission check and history data
* @throws IOException if error occurs
* @throws CancelledException if database creation cancelled by user
*/
LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, BufferFile srcFile,
String contentType, String fileID, String comment, boolean resetDatabaseId,
TaskMonitor monitor, String user) throws IOException, CancelledException {
super(fileSystem, propertyFile, true, true);
boolean success = false;
long checkoutId = DEFAULT_CHECKOUT_ID;
try {
if (fileID != null) {
String oldFileId = propertyFile.getFileID();
propertyFile.setFileID(fileID);
fileSystem.fileIdChanged(propertyFile, oldFileId);
}
propertyFile.putInt(FILE_TYPE, DATABASE_FILE_TYPE);
propertyFile.putBoolean(READ_ONLY, false);
propertyFile.putString(CONTENT_TYPE, contentType);
if (isVersioned) {
ItemCheckoutStatus coStatus = checkout(CheckoutType.NORMAL, user, null);
checkoutId = coStatus.getCheckoutId();
beginCheckin(checkoutId);
versionedDbListener = new LocalVersionedDbListener();
versionedDb = new VersionedDatabase(getDataDir(), srcFile, versionedDbListener,
checkoutId, comment, monitor);
versionedDb.setSynchronizationObject(fileSystem);
terminateCheckout(checkoutId, false);
}
else {
privateDb = new PrivateDatabase(getDataDir(), srcFile, resetDatabaseId, monitor);
privateDb.setIsCheckoutCopy(isCheckedOut());
privateDb.setSynchronizationObject(fileSystem);
}
propertyFile.writeState();
success = true;
}
finally {
if (!success) {
if (isVersioned) {
endCheckin(checkoutId);
}
abortCreate();
}
}
fireItemCreated();
}
/**
* Create a new local Database item which corresponds to the specified
* property file. The initial contents of the database are copied from the
* specified packedFile.
* @param fileSystem file system
* @param propertyFile database property file
* @param packedFile packed database file
* @param contentType user content type
* @param monitor copy progress monitor
* @param user if versioned, user used for permission check and history data
* @throws IOException if error occurs
* @throws CancelledException if database creation cancelled by user
*/
LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, File packedFile,
String contentType, TaskMonitor monitor, String user)
throws IOException, CancelledException {
super(fileSystem, propertyFile, true, true);
if (isVersioned) {
// no supported use case
throw new UnsupportedOperationException();
}
boolean success = false;
long checkoutId = DEFAULT_CHECKOUT_ID;
try {
propertyFile.putInt(FILE_TYPE, DATABASE_FILE_TYPE);
propertyFile.putBoolean(READ_ONLY, false);
propertyFile.putString(CONTENT_TYPE, contentType);
String oldFileId = propertyFile.getFileID();
propertyFile.setFileID(FileIDFactory.createFileID());
fileSystem.fileIdChanged(propertyFile, oldFileId);
// if (isVersioned) {
// unsupported operation
// ItemCheckoutStatus coStatus = checkout(false, user, null);
// checkoutId = coStatus.getCheckoutId();
// beginCheckin(checkoutId);
// String comment = "Unpacked " + packedFile;
// versionedDbListener = new LocalVersionedDbListener();
// versionedDb =
// new VersionedDatabase(getDataDir(), packedFile, versionedDbListener, checkoutId, comment,
// monitor);
// versionedDb.setSynchronizationObject(fileSystem);
// terminateCheckout(checkoutId, false);
// }
// else {
privateDb = new PrivateDatabase(getDataDir(), packedFile, monitor);
privateDb.setIsCheckoutCopy(isCheckedOut());
privateDb.setSynchronizationObject(fileSystem);
// }
propertyFile.writeState();
success = true;
}
finally {
if (!success) {
if (isVersioned) {
endCheckin(checkoutId);
}
abortCreate();
}
}
fireItemCreated();
}
/**
* Create a new LocalDatabaseItem and an empty updateable BufferFile which may be used
* to create the initial database content.
* @param fileSystem file system
* @param propertyFile database property file
* @param bufferSize buffer size to be used for new database
* @param contentType user content type
* @param fileID unique file ID or null
* @param user if versioned, user used for permission check and history data
* @param projectPath path of project in versioned database checkout is done (may be null for non-versioned database)
* @return open updateable empty BufferFile for initial content writing
* @throws IOException if error occurs
*/
static LocalManagedBufferFile create(final LocalFileSystem fileSystem,
PropertyFile propertyFile, int bufferSize, String contentType, String fileID,
String user, String projectPath) throws IOException {
final LocalDatabaseItem dbItem = new LocalDatabaseItem(fileSystem, propertyFile, true);
File dbDir = dbItem.getDataDir();
long checkoutId = DEFAULT_CHECKOUT_ID;
boolean success = false;
try {
if (fileID != null) {
String oldFileId = propertyFile.getFileID();
propertyFile.setFileID(fileID);
fileSystem.fileIdChanged(propertyFile, oldFileId);
}
propertyFile.putInt(FILE_TYPE, DATABASE_FILE_TYPE);
propertyFile.putBoolean(READ_ONLY, false);
propertyFile.putString(CONTENT_TYPE, contentType);
LocalManagedBufferFile bfile;
if (fileSystem.isVersioned()) {
ItemCheckoutStatus coStatus =
dbItem.checkout(CheckoutType.NORMAL, user, projectPath);
checkoutId = coStatus.getCheckoutId();
dbItem.beginCheckin(checkoutId);
bfile = VersionedDatabase.createVersionedDatabase(dbDir, bufferSize,
dbItem.versionedDbListener, checkoutId);
}
else {
bfile = PrivateDatabase.createDatabase(dbDir, (db, version) -> {
synchronized (fileSystem) {
if (version == 1) {
if (dbItem.privateDb == null) {
db.setSynchronizationObject(dbItem.fileSystem);
dbItem.privateDb = (PrivateDatabase) db;
}
dbItem.fireItemCreated();
}
}
}, bufferSize);
}
propertyFile.writeState();
success = true;
return bfile;
}
finally {
if (!success) {
if (fileSystem.isVersioned()) {
dbItem.endCheckin(checkoutId);
}
dbItem.abortCreate();
}
}
}
@Override
public long length() throws IOException {
if (isVersioned) {
return versionedDb.length();
}
return privateDb.length();
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#moveTo(java.io.File, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
void moveTo(File newFolder, String newStorageName, String newFolderPath, String newName)
throws IOException {
super.moveTo(newFolder, newStorageName, newFolderPath, newName);
if (isVersioned) {
versionedDb.dbMoved(getDataDir());
}
else {
privateDb.dbMoved(getDataDir());
}
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#fireItemChanged()
*/
@Override
void fireItemChanged() {
if (privateDb != null) {
privateDb.setIsCheckoutCopy(isCheckedOut());
}
super.fireItemChanged();
}
/*
* @see ghidra.framework.store.FolderItem#getCurrentVersion()
*/
@Override
public int getCurrentVersion() {
if (isVersioned) {
return versionedDb != null ? versionedDb.getCurrentVersion() : 0;
}
return privateDb != null ? privateDb.getCurrentVersion() : 0;
}
/*
* @see ghidra.framework.store.FolderItem#getMinimumVersion()
*/
@Override
public int getMinimumVersion() throws IOException {
// The database object may be null during initial database creation,
// although such an instance is private to the static create method.
if (isVersioned) {
if (versionedDb == null) {
throw new IllegalStateException();
}
return versionedDb.getMinimumVersion();
}
if (privateDb == null) {
throw new IllegalStateException();
}
return privateDb.getCurrentVersion();
}
/**
* <code>LocalVersionedDbListener</code> provides a listener
* which maintains checkout and history data in response to
* VersionedDatabase callbacks.
*/
private class LocalVersionedDbListener implements VersionedDBListener {
/*
* @see ghidra.framework.store.db.VersionedDBListener#versionsChanged(int, int)
*/
@Override
public void versionsChanged(int minVersion, int currentVersion) {
synchronized (fileSystem) {
if (minVersion == 0 && currentVersion == 0) {
// file must have been removed
}
try {
if (historyMgr.fixHistory(minVersion, currentVersion)) {
fireItemChanged();
}
}
catch (IOException e) {
Msg.error(this, "Failed to update version history: " + getPathName(), e);
}
}
}
/*
* @see ghidra.framework.store.db.VersionedDBListener#versionCreated(ghidra.framework.store.db.VersionedDatabase, int, long, java.lang.String, long)
*/
@Override
public boolean versionCreated(VersionedDatabase database, int version, long time,
String comment, long dbCheckinId) {
synchronized (fileSystem) {
try {
ItemCheckoutStatus coStatus = checkoutMgr.getCheckout(dbCheckinId);
if (coStatus == null || LocalDatabaseItem.this.checkinId != dbCheckinId) {
log("ERROR! version " + version + " created without valid checkin", null);
return false;
}
if (version == 1 && versionedDb == null) {
versionedDb = database;
versionedDb.setSynchronizationObject(fileSystem);
}
String user = coStatus.getUser();
historyMgr.versionAdded(version, time, comment, user);
checkoutMgr.updateCheckout(dbCheckinId, version);
}
catch (IOException e) {
Msg.error(getName() + " versioning error", e);
}
}
if (version == 1) {
fireItemCreated();
}
else {
fireItemChanged();
}
return true;
}
/*
* @see ghidra.framework.store.db.VersionedDBListener#versionDeleted(int)
*/
@Override
public void versionDeleted(int version) {
synchronized (fileSystem) {
try {
historyMgr.versionDeleted(version, deleteUser);
}
catch (IOException e) {
Msg.error(this, "Failed to update version history: " + getPathName(), e);
}
}
}
/*
* @see ghidra.framework.store.db.VersionedDBListener#getCheckoutVersion(long)
*/
@Override
public int getCheckoutVersion(long checkoutId) throws IOException {
synchronized (fileSystem) {
ItemCheckoutStatus coStatus = checkoutMgr.getCheckout(checkoutId);
return coStatus != null ? coStatus.getCheckoutVersion() : -1;
}
}
/*
* @see ghidra.framework.store.db.VersionedDBListener#checkinCompleted(long)
*/
@Override
public void checkinCompleted(long dbCheckinId) {
synchronized (fileSystem) {
if (isVersioned) {
endCheckin(dbCheckinId);
}
if (versionedDb == null || versionedDb.getCurrentVersion() == 0) {
// remove item which was created during initial creation
try {
if (isVersioned) {
checkoutMgr.endCheckout(dbCheckinId);
}
deleteContent(null);
fileSystem.itemDeleted(getParentPath(), getName()); // de-allocates index entry
fileSystem.deleteEmptyVersionedFolders(getParentPath());
}
catch (IOException e) {
Msg.error(this, getName() + " versioning error", e);
}
}
}
}
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteMinimumVersion()
*/
@Override
void deleteMinimumVersion(String user) throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
deleteUser = user;
versionedDb.deleteMinimumVersion();
}
}
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteCurrentVersion()
*/
@Override
void deleteCurrentVersion(String user) throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
deleteUser = user;
versionedDb.deleteCurrentVersion();
}
}
}
/*
* @see ghidra.framework.store.DatabaseItem#open(int, int)
*/
@Override
public LocalManagedBufferFile open(int version, int minChangeDataVer) throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
return versionedDb.openBufferFile(version, minChangeDataVer);
}
if (version == LATEST_VERSION) {
return privateDb.openBufferFile();
}
throw new IllegalArgumentException("only LATEST_VERSION may be opened");
}
}
/*
* @see ghidra.framework.store.DatabaseItem#open(int)
*/
@Override
public LocalManagedBufferFile open(int version) throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
return versionedDb.openBufferFile(version, -1);
}
if (version == LATEST_VERSION) {
return privateDb.openBufferFile();
}
throw new IllegalArgumentException("only LATEST_VERSION may be opened");
}
}
/*
* @see ghidra.framework.store.DatabaseItem#open()
*/
@Override
public LocalManagedBufferFile open() throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
return versionedDb.openBufferFile(LATEST_VERSION, -1);
}
return privateDb.openBufferFile();
}
}
/**
* Open the latest database version for update.
* @param checkoutId reqiured for update to a versioned item, otherwise set to -1 for
* a non-versioned private database.
* @return open database handle
* @throws IOException
*/
@Override
public LocalManagedBufferFile openForUpdate(long checkoutId) throws IOException {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
if (isVersioned) {
beginCheckin(checkoutId);
boolean success = false;
try {
LocalManagedBufferFile bfile = versionedDb.openBufferFileForUpdate(checkoutId);
success = true;
return bfile;
}
finally {
if (!success) {
endCheckin(checkoutId);
}
}
}
return privateDb.openBufferFileForUpdate();
}
}
/**
* @see ghidra.framework.store.FolderItem#canRecover()
*/
@Override
public boolean canRecover() {
synchronized (fileSystem) {
return privateDb != null && privateDb.canRecover();
}
}
/*
* @see ghidra.framework.store.FolderItem#output(java.io.File, int version, ghidra.util.task.TaskMonitor)
*/
@Override
public void output(File outputFile, int version, TaskMonitor monitor)
throws CancelledException, IOException {
synchronized (fileSystem) {
if (isVersioned) {
versionedDb.output(version, outputFile, getName(), DATABASE_FILE_TYPE,
getContentType(), monitor);
}
else {
privateDb.output(outputFile, getName(), DATABASE_FILE_TYPE, getContentType(),
monitor);
}
}
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, boolean, ghidra.util.task.TaskMonitor)
*/
@Override
public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
TaskMonitor monitor) throws IOException, CancelledException {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
long checkoutId = getCheckoutId();
boolean exclusive = isCheckedOutExclusive();
if (isVersioned || checkoutId == DEFAULT_CHECKOUT_ID) {
throw new IOException(getName() + " is not checked-out");
}
DatabaseItem verDbItem = (DatabaseItem) versionedFolderItem;
//ItemCheckoutStatus coStatus = verDbItem.getCheckout(checkoutId);
//int coVer = coStatus.getCheckoutVersion();
int ver = verDbItem.getCurrentVersion();
if (updateItem) {
ManagedBufferFile verBf = verDbItem.open(ver);
try {
privateDb.updateCheckoutCopy(verBf, getCheckoutVersion(), monitor);
}
finally {
try {
verBf.close();
}
catch (IOException e) {
// ignored
}
}
}
else {
privateDb.updateCheckoutCopy();
}
setCheckout(checkoutId, exclusive, ver, getCurrentVersion());
}
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, int)
*/
@Override
public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
long checkoutId = getCheckoutId();
if (isVersioned || checkoutId == DEFAULT_CHECKOUT_ID) {
throw new IOException(getName() + " is not checked-out");
}
if (!(item instanceof LocalDatabaseItem)) {
throw new IllegalArgumentException("Expected local database item");
}
LocalDatabaseItem dbItem = (LocalDatabaseItem) item;
if (fileSystem != dbItem.fileSystem) {
throw new IllegalArgumentException("Items must be from same file system");
}
try {
privateDb.updateCheckoutFrom(dbItem.privateDb);
setCheckout(checkoutId, isCheckedOutExclusive(), checkoutVersion,
getLocalCheckoutVersion());
}
finally {
item.delete(-1, null);
}
}
}
/*
* @see ghidra.framework.store.FolderItem#lastModified()
*/
@Override
public long lastModified() {
if (privateDb != null) {
return privateDb.lastModified();
}
return versionedDb.lastModified();
}
/*
* @see ghidra.framework.store.FolderItem#refresh()
*/
@Override
public LocalFolderItem refresh() throws IOException {
if (super.refresh() == null) {
return null;
}
if (isVersioned) {
versionedDb.refresh();
}
else {
privateDb.refresh();
privateDb.setIsCheckoutCopy(isCheckedOut());
}
return this;
}
static void cleanupOldPresaveFiles(File root) {
Thread t = new Thread(new CleanupRunnable(root), "Database-Item-Cleanup");
t.start();
}
private static class CleanupRunnable implements Runnable {
private File root;
CleanupRunnable(File root) {
this.root = root;
}
@Override
public void run() {
// Determine current filesystem time
File f;
try {
f = File.createTempFile("tmp", ".tmp", root);
long now = f.lastModified();
f.delete();
cleanupDir(root, now);
}
catch (IOException e) {
Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
}
}
private void cleanupDir(File dir, long beforeNow) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if (f.isDirectory()) {
String fname = f.getName();
if (!LocalFileSystem.isHiddenDirName(fname)) {
cleanupDir(f, beforeNow);
}
else if (fname.endsWith(DATA_DIR_EXTENSION)) {
LocalBufferFile.cleanupOldPreSaveFiles(f, beforeNow);
}
}
}
}
}
}
}

View file

@ -0,0 +1,910 @@
/* ###
* 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.store.local;
import java.io.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import db.buffers.BufferFile;
import db.buffers.LocalManagedBufferFile;
import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
/**
* <code>LocalFileSystem</code> provides access to FolderItem's which
* exist within a File-based directory structure. Although FolderItem
* caching is highly recommended, it is not provided by this implementation
* and should be provided by an encompassing set of folder/file objects.
* <p>
* A LocalFileSystem may optionally support version control of its
* FolderItem's. When versioned, FolderItem's must be checked-out
* to create new versions. When not versioned, the check-out mechanism
* is not used.
* <p>
* FileSystemListener's will only be notified of changes made by the
* associated LocalFileSystem instance. For this reason, it is important
* that proper measures are taken to prevent concurrent modification of the
* underlying files/directories by another instance or by any other
* means.
*/
public abstract class LocalFileSystem implements FileSystem {
static final Logger log = LogManager.getLogger(LocalFileSystem.class);
/**
* Hidden directory name prefix.
* Should only be prepended to an escaped base-name.
* @see #escapeHiddenPrefixChars(String)
*/
public static final char HIDDEN_DIR_PREFIX_CHAR = '~';
public static final String HIDDEN_DIR_PREFIX = Character.toString(HIDDEN_DIR_PREFIX_CHAR);
/**
* Hidden item name prefix.
*/
public static final String HIDDEN_ITEM_PREFIX = ".ghidra.";
// NOTE: The / and : chars are reserved for use by the file system and should always be disallowed!
private static final String INVALID_FILENAME_CHARS = "/\\'`\"*:<>?|";
static final String PROPERTY_EXT = PropertyFile.PROPERTY_EXT;
// private static final int MAX_PATHNAME_LENGTH = 255;
private static boolean refreshRequired = false;
protected final File root;
protected final boolean isVersioned;
protected final boolean readOnly;
protected final FileSystemListenerList listeners;
private RepositoryLogger repositoryLogger;
// Always false in production; can be manipulated by tests
private boolean isShared;
/**
* Construct a local filesystem for existing data
* @param rootPath
* @param create
* @param isVersioned
* @param readOnly
* @param enableAsyncronousDispatching
* @return local filesystem
* @throws FileNotFoundException if specified rootPath does not exist
* @throws IOException if error occurs while reading/writing index files
*/
public static LocalFileSystem getLocalFileSystem(String rootPath, boolean create,
boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching)
throws IOException {
File root = new File(rootPath);
if (!root.isDirectory()) {
throw new IOException("filesystem directory not found: " + rootPath);
}
if (create && root.list().length != 0) {
throw new IOException("new filesystem directory is not empty: " + rootPath);
}
if (create) {
// if (isCreateMangledFileSystemEnabled()) {
// return new MangledLocalFileSystem(rootPath, isVersioned, readOnly,
// enableAsyncronousDispatching);
// }
return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, true);
}
else if (!readOnly && !root.canWrite()) {
throw new IOException("filesystem directory is not writable: " + rootPath);
}
int indexVersion = -1;
if (IndexedLocalFileSystem.isIndexed(rootPath)) {
indexVersion = IndexedLocalFileSystem.readIndexVersion(rootPath);
}
else if (IndexedLocalFileSystem.hasIndexedStructure(rootPath)) {
// assume latest version - index file missing, rebuild required
indexVersion = IndexedLocalFileSystem.LATEST_INDEX_VERSION;
}
switch (indexVersion) {
case -1:
if (hasAnyHiddenFiles(root)) {
throw new IOException("Unsupported file system schema: " + rootPath);
}
// Use legacy mangled filesystem if existing data does not appear to be indexed
Msg.warn(LocalFileSystem.class, "Using deprecated Mangled filesystem: " + rootPath);
return new MangledLocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching);
case 0:
Msg.warn(LocalFileSystem.class,
"Using deprecated Indexed filesystem (V0): " + rootPath);
return IndexedLocalFileSystem.getFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching);
case 1:
return IndexedV1LocalFileSystem.getFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching);
default:
throw new IOException(
"Unsupported file system version (" + indexVersion + "): " + rootPath);
}
}
@Override
public String getUserName() {
return SystemUtilities.getUserName();
}
/**
* Returns true if any file found within dir whose name starts
* with '~' character (e.g., ~index.dat, etc)
* @param dir
* @return true if any hidden file found with '~' prefix
*/
private static boolean hasAnyHiddenFiles(File dir) {
for (File f : dir.listFiles()) {
if (f.getName().startsWith("~") && f.isFile()) {
return true;
}
}
return false;
}
/**
* Constructor.
* @param file path path for root directory.
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws FileNotFoundException if specified rootPath does not exist
*/
protected LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
boolean enableAsyncronousDispatching) throws FileNotFoundException {
root = new File(rootPath);
if (!root.isDirectory()) {
throw new FileNotFoundException("data directory not found: " + rootPath);
}
this.isVersioned = isVersioned;
this.readOnly = readOnly;
listeners = new FileSystemListenerList(enableAsyncronousDispatching);
}
protected void cleanupAfterConstruction() {
if (!readOnly) {
LocalDatabaseItem.cleanupOldPresaveFiles(root);
cleanupTemporaryFiles(SEPARATOR);
}
}
/**
* Constructor for an empty read-only file-system.
*/
protected LocalFileSystem() {
this.root = null;
this.isVersioned = false;
this.readOnly = true;
listeners = null;
}
private void cleanupTemporaryFiles(String folderPath) {
try {
for (String itemName : getItemNames(folderPath, true)) {
if (!itemName.startsWith(HIDDEN_ITEM_PREFIX)) {
continue;
}
LocalFolderItem item = getItem(folderPath, itemName);
if (item != null) {
item.deleteContent(null);
}
else {
// make sure we get item out of index
deallocateItemStorage(folderPath, itemName);
}
}
String parentPath = folderPath + (folderPath.endsWith(SEPARATOR) ? "" : SEPARATOR);
for (String subfolder : getFolderNames(folderPath)) {
cleanupTemporaryFiles(parentPath + subfolder);
}
}
catch (FileNotFoundException e) {
// ignore
}
catch (IOException e) {
e.printStackTrace();
// ignore
}
}
/**
* Associate file system with a specific repository logger
* @param repositoryLogger
*/
public void setAssociatedRepositoryLogger(RepositoryLogger repositoryLogger) {
this.repositoryLogger = repositoryLogger;
}
protected void log(LocalFolderItem item, String msg, String user) {
String path = item != null ? item.getPathName() : null;
if (repositoryLogger != null) {
repositoryLogger.log(path, msg, user);
}
else {
StringBuffer buf = new StringBuffer();
if (item != null) {
buf.append(item.getPathName());
}
buf.append(": ");
buf.append(msg);
if (user != null) {
buf.append(" (");
buf.append(user);
buf.append(")");
}
log.info(buf.toString());
}
}
/**
* If set, the state of folder item resources will be continually refreshed.
* This is required if multiple instances exist for a single item. The default is
* disabled. This feature should be enabled for testing only since it may have a
* significant performance impact. This does not provide locking which may be
* required for a shared environment (e.g., checkin locking is only managed by a
* single instance).
* @param validationRequired
*/
public static void setValidationRequired() {
refreshRequired = true;
}
/**
* @returns true if folder item resources must be refreshed.
* @see #setValidationRequired()
*/
public static boolean isRefreshRequired() {
return refreshRequired;
}
/*
* @see ghidra.framework.store.FileSystem#isVersioned()
*/
@Override
public boolean isVersioned() {
return isVersioned;
}
/*
* @see ghidra.framework.store.FileSystem#isOnline()
*/
@Override
public boolean isOnline() {
return true;
}
/*
* @see ghidra.framework.store.FileSystem#isReadOnly()
*/
@Override
public boolean isReadOnly() {
return readOnly;
}
protected static class ItemStorage {
File dir;
String storageName;
String folderPath;
String itemName;
ItemStorage(File dir, String storageName, String folderPath, String itemName) {
this.dir = dir;
this.storageName = storageName;
this.folderPath = folderPath;
this.itemName = itemName;
}
boolean exists() {
File pfile = new File(dir, storageName + PROPERTY_EXT);
return pfile.exists();
}
PropertyFile getPropertyFile() throws IOException {
return new PropertyFile(dir, storageName, folderPath, itemName);
}
@Override
public String toString() {
String path;
try {
path = getPropertyFile().getPath();
}
catch (IOException e) {
path = "<ERROR: " + e.getMessage() + ">";
}
return itemName + " (" + storageName + ", " + path + ")";
}
}
/**
* Find an existing storage location
* @param folderPath
* @param itemName
* @return storage location. A non-null value does not guarantee that the associated
* item actually exists.
* @throws FileNotFoundException
*/
protected abstract ItemStorage findItemStorage(String folderPath, String itemName)
throws FileNotFoundException;
/**
* Allocate a new storage location
* @param folderPath
* @param itemName
* @return storage location
* @throws DuplicateFileException if item path has previously been allocated
* @throws IOException if invalid path/item name specified
* @throws InvalidNameException if folderPath or itemName contains invalid characters
*/
protected abstract ItemStorage allocateItemStorage(String folderPath, String itemName)
throws IOException, InvalidNameException;
/**
* Deallocate item storage
* @param folderPath
* @param itemName
* @throws IOException
*/
protected abstract void deallocateItemStorage(String folderPath, String itemName)
throws IOException;
protected abstract String[] getItemNames(String folderPath, boolean includeHiddenFiles)
throws IOException;
/**
*
* @see ghidra.framework.store.FileSystem#getItemNames(java.lang.String)
*/
@Override
public synchronized String[] getItemNames(String folderPath) throws IOException {
return getItemNames(folderPath, false);
}
/*
* @see ghidra.framework.store.FileSystem#getItem(java.lang.String, java.lang.String)
*/
@Override
public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException {
try {
ItemStorage itemStorage = findItemStorage(folderPath, name);
if (itemStorage == null) {
return null;
}
PropertyFile propertyFile = itemStorage.getPropertyFile();
if (propertyFile.exists()) {
return LocalFolderItem.getFolderItem(this, propertyFile);
}
}
catch (FileNotFoundException e) {
// ignore
}
return null;
}
/**
* Notification that FileID has been changed within propertyFile
* @param propertyFile
* @param oldFileId
* @throws IOException
*/
protected void fileIdChanged(PropertyFile propertyFile, String oldFileId) throws IOException {
// do nothing by default
}
@Override
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
throw new UnsupportedOperationException("getItem by File-ID");
}
/*
* @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, db.buffers.BufferFile, java.lang.String, java.lang.String, boolean, ghidra.util.task.TaskMonitor, java.lang.String)
*/
@Override
public synchronized LocalDatabaseItem createDatabase(String parentPath, String name,
String fileID, BufferFile bufferFile, String comment, String contentType,
boolean resetDatabaseId, TaskMonitor monitor, String user)
throws InvalidNameException, IOException, CancelledException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(name, false);
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalDatabaseItem item = null;
try {
PropertyFile propertyFile = itemStorage.getPropertyFile();
item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID,
comment, resetDatabaseId, monitor, user);
}
finally {
if (item == null) {
deallocateItemStorage(parentPath, name);
}
}
return item;
}
public synchronized LocalDatabaseItem createTemporaryDatabase(String parentPath, String name,
String fileID, BufferFile bufferFile, String contentType, boolean resetDatabaseId,
TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(name, false);
String hiddenName = HIDDEN_ITEM_PREFIX + name;
ItemStorage itemStorage = allocateItemStorage(parentPath, hiddenName);
LocalDatabaseItem item = null;
try {
PropertyFile propertyFile = itemStorage.getPropertyFile();
item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID, null,
resetDatabaseId, monitor, null);
}
finally {
if (item == null) {
deallocateItemStorage(parentPath, name);
}
}
return item;
}
/*
* @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String)
*/
@Override
public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath)
throws InvalidNameException, IOException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(name, false);
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalManagedBufferFile bufferFile = null;
try {
PropertyFile propertyFile = itemStorage.getPropertyFile();
bufferFile = LocalDatabaseItem.create(this, propertyFile, bufferSize, contentType,
fileID, user, projectPath);
}
finally {
if (bufferFile == null) {
deallocateItemStorage(parentPath, name);
}
}
return bufferFile;
}
/*
* @see ghidra.framework.store.FileSystem#createDataFile(java.lang.String, java.lang.String, java.io.InputStream, java.lang.String, java.lang.String, ghidra.util.task.TaskMonitor)
*/
@Override
public synchronized LocalDataFile createDataFile(String parentPath, String name,
InputStream istream, String comment, String contentType, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(name, false);
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalDataFile dataFile = null;
try {
//TODO handle comment
PropertyFile propertyFile = itemStorage.getPropertyFile();
dataFile = new LocalDataFile(this, propertyFile, istream, contentType, monitor);
}
finally {
if (dataFile == null) {
deallocateItemStorage(parentPath, name);
}
}
listeners.itemCreated(parentPath, name);
return dataFile;
}
/*
* @see ghidra.framework.store.FileSystem#createFile(java.lang.String, java.lang.String, java.io.File, ghidra.util.task.TaskMonitor, java.lang.String)
*/
@Override
public LocalDatabaseItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user)
throws InvalidNameException, IOException, CancelledException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(name, false);
ItemDeserializer itemDeserializer = new ItemDeserializer(packedFile);
String contentType;
try {
int fileType = itemDeserializer.getFileType();
if (fileType != FolderItem.DATABASE_FILE_TYPE) {
throw new UnsupportedOperationException("Only packed database files are supported");
}
if (name == null) {
name = itemDeserializer.getItemName();
}
contentType = itemDeserializer.getContentType();
}
finally {
itemDeserializer.dispose();
}
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalDatabaseItem item = null;
try {
PropertyFile propertyFile = itemStorage.getPropertyFile();
item =
new LocalDatabaseItem(this, propertyFile, packedFile, contentType, monitor, user);
}
finally {
if (item == null) {
deallocateItemStorage(parentPath, name);
}
}
return item;
}
/*
* @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public synchronized void moveItem(String folderPath, String name, String newFolderPath,
String newName) throws IOException, InvalidNameException {
if (readOnly) {
throw new ReadOnlyException();
}
ItemStorage itemStorage = findItemStorage(folderPath, name);
LocalFolderItem item = getItem(folderPath, name);
if (itemStorage == null || item == null) {
throw new FileNotFoundException(
"Item " + name + " in folder " + folderPath + " not found");
}
if (folderPath.equals(newFolderPath) && name.equals(newName)) {
return;
}
testValidName(newFolderPath, true);
testValidName(newName, false);
item.checkInUse();
ItemStorage newStorage = null;
boolean success = false;
try {
newStorage = allocateItemStorage(newFolderPath, newName);
item.moveTo(newStorage.dir, newStorage.storageName, newFolderPath, newName);
deallocateItemStorage(folderPath, name);
success = true;
if (folderPath.equals(newFolderPath)) {
listeners.itemRenamed(folderPath, name, newName);
}
else {
listeners.itemMoved(folderPath, name, newFolderPath, newName);
}
deleteEmptyVersionedFolders(folderPath);
deallocateItemStorage(folderPath, name);
}
finally {
if (!success) {
if (newStorage != null) {
deallocateItemStorage(newFolderPath, newName);
}
deleteEmptyVersionedFolders(newFolderPath);
}
}
}
@Override
public abstract boolean folderExists(String folderPath);
/*
* @see ghidra.framework.store.FileSystem#fileExists(java.lang.String, java.lang.String)
*/
@Override
public boolean fileExists(String folderPath, String name) {
try {
ItemStorage itemStorage = findItemStorage(folderPath, name);
if (itemStorage == null) {
return false;
}
return itemStorage.exists();
}
catch (IOException e) {
return false;
}
}
/*
* @see ghidra.framework.store.FileSystem#addFileSystemListener(ghidra.framework.store.FileSystemListener)
*/
@Override
public void addFileSystemListener(FileSystemListener listener) {
if (listeners != null) {
listeners.add(listener);
}
}
/*
* @see ghidra.framework.store.FileSystem#removeFileSystemListener(ghidra.framework.store.FileSystemListener)
*/
@Override
public void removeFileSystemListener(FileSystemListener listener) {
if (listeners != null) {
listeners.remove(listener);
}
}
/**
* Returns file system listener.
*/
FileSystemListener getListener() {
return listeners;
}
/**
* @returns the maximum name length permitted for folders or items.
*/
public abstract int getMaxNameLength();
/**
* Validate a folder/item name or path.
* @param name folder or item name
* @param isPath if true name represents full path
* @throws InvalidNameException if name is invalid
*/
public void testValidName(String name, boolean isPath) throws InvalidNameException {
if (name == null || name.length() == 0) {
throw new InvalidNameException("path or name is empty or null");
}
if (isPath) {
if (name.equals(SEPARATOR)) {
return;
}
if (name.startsWith(SEPARATOR)) {
name = name.substring(1);
}
String[] splitName = name.split(SEPARATOR);
for (String element : splitName) {
testValidName(element, false);
}
return;
}
if (!isPath && name.length() > getMaxNameLength()) {
throw new InvalidNameException("Project file names within Ghidra must be less than " +
getMaxNameLength() + " characters in length.");
}
if (name.startsWith(HIDDEN_ITEM_PREFIX)) {
throw new InvalidNameException(
name + " starts with a reserved prefix '" + HIDDEN_ITEM_PREFIX + "'");
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!isValidNameCharacter(c)) {
throw new InvalidNameException(
name + " contains an invalid character: \'" + c + "\'");
}
}
}
/**
* @return true if c is a valid character within the FileSystem.
*/
public static boolean isValidNameCharacter(char c) {
return !((c < ' ') || (INVALID_FILENAME_CHARS.indexOf(c) >= 0) || (c > 255));
}
/**
* Remove the directory which corresponds to the specified folder path if it is empty.
* If folder directory is removed, this method is invoked recursively for parent folder
* path which may also be removed if it is empty.
* This method is intended for use with a versioned file system
* which only retains folders if they contain one or more items or sub-folders.
* @param folderPath folder path
*/
protected synchronized void deleteEmptyVersionedFolders(String folderPath) {
try {
if (isVersioned) {
if (folderPath.length() == 1) {
return;
}
String[] items = getItemNames(folderPath);
if (items.length > 0) {
return;
}
String[] folders = getFolderNames(folderPath);
if (folders.length > 0) {
return;
}
deleteFolder(folderPath);
deleteEmptyVersionedFolders(getParentPath(folderPath));
}
}
catch (IOException e) {
// ignore
}
}
/**
* Notify the filesystem that the property file and associated data files for
* an item have been removed from the filesystem.
* @param folderPath
* @param itemName
* @throws IOException
*/
protected synchronized void itemDeleted(String folderPath, String itemName) throws IOException {
// do nothing
}
/**
* Returns the full path for a specific folder or item
* @param parentPath full parent path
* @param name child folder or item name
*/
protected final static String getPath(String parentPath, String name) {
if (parentPath.length() == 1) {
return parentPath + name;
}
return parentPath + SEPARATOR_CHAR + name;
}
protected final static String getParentPath(String path) {
if (path.length() == 1) {
return null;
}
int index = path.lastIndexOf(SEPARATOR_CHAR);
if (index == 0) {
return SEPARATOR;
}
return path.substring(0, index);
}
protected final static String getName(String path) {
if (path.length() == 1) {
return path;
}
if (path.endsWith(SEPARATOR)) {
path = path.substring(0, path.length() - 1);
}
return path.substring(path.lastIndexOf(SEPARATOR_CHAR) + 1);
}
@Override
public boolean isShared() {
// Does not support direct sharing in production
return isShared;
}
// static void testValidPathLength(File file) throws IOException {
// String path = file.getAbsolutePath();
// if (path.length() + LocalFolderItem.DATA_DIR_EXTENSION.length() > MAX_PATHNAME_LENGTH) {
// throw new IOException("Length of path name for file exceeds maximum of " +
// MAX_PATHNAME_LENGTH);
// }
// }
@Override
public void dispose() {
if (listeners != null) {
listeners.dispose();
}
}
public boolean migrationInProgress() {
return false;
}
/**
* Determines if the specified storage name corresponds to a hidden name
* @param name
* @return true if name is a hidden name
*/
public static final boolean isHiddenDirName(String name) {
// odd number of prefix chars at start of name indicates hidden name
return (countHiddenDirPrefixChars(name) & 1) == 1;
}
/**
* Escape hidden prefix chars in name
* @param name
* @return escaped name
*/
public static final String escapeHiddenDirPrefixChars(String name) {
int prefixCount = countHiddenDirPrefixChars(name);
if (prefixCount == 0) {
return name;
}
StringBuilder buf = new StringBuilder();
// keep number of hidden prefix chars even
for (int i = 0; i < prefixCount; ++i) {
buf.append(HIDDEN_DIR_PREFIX_CHAR);
}
buf.append(name);
return buf.toString();
}
/**
* Unescape a non-hidden directory name
* @param name
* @return unescaped name or null if name is a hidden name
*/
public static final String unescapeHiddenDirPrefixChars(String name) {
int prefixCount = countHiddenDirPrefixChars(name);
if ((prefixCount & 1) == 1) {
return null;
}
prefixCount = prefixCount >> 1;
return name.substring(prefixCount);
}
private static int countHiddenDirPrefixChars(String name) {
int count = 0;
int length = name.length();
for (int index = 0; index < length &&
name.charAt(index) == HIDDEN_DIR_PREFIX_CHAR; index++) {
++count;
}
return count;
}
}

View file

@ -0,0 +1,108 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import java.io.File;
import java.io.IOException;
public class LocalFilesystemTestUtils {
private LocalFilesystemTestUtils() {
}
/**
* Create empty mangled filesystem
* @param rootPath path for root directory (must already exist).
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws IOException
*/
public static MangledLocalFileSystem createMangledFilesystem(String rootPath,
boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching)
throws IOException {
createRootDir(rootPath);
return new MangledLocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching);
}
/**
* Create empty original Indexed filesystem. The original index file lacked any version indicator
* but will be treated as a version 0 index.
* @param rootPath path for root directory (must already exist).
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws IOException
*/
public static IndexedLocalFileSystem createOriginalIndexedFilesystem(String rootPath,
boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching)
throws IOException {
createRootDir(rootPath);
return null;
}
/**
* Create empty V0 Indexed filesystem. This is an original Indexed filesystem with the addition
* of a version 0 indicator within the index file.
* @param rootPath path for root directory (must already exist).
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws IOException
*/
public static IndexedLocalFileSystem createIndexedV0Filesystem(String rootPath,
boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching)
throws IOException {
createRootDir(rootPath);
return new IndexedLocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, true);
}
/**
* Create empty mangled filesystem
* @param rootPath path for root directory (must already exist).
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws IOException
*/
public static IndexedV1LocalFileSystem createIndexedV1Filesystem(String rootPath,
boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching)
throws IOException {
createRootDir(rootPath);
return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, true);
}
private static void createRootDir(String rootPath) throws IOException {
File dir = new File(rootPath);
if (!dir.isDirectory() && !dir.mkdirs()) {
throw new IOException("Failed to create root directory: " + dir);
}
}
}

View file

@ -0,0 +1,897 @@
/* ###
* 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.store.local;
import java.io.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.framework.store.*;
import ghidra.util.*;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
/**
* <code>LocalFolderItem</code> provides an abstract implementation of a folder
* item which resides on a local file-system. An item is defined by a property file
* and generally has a hidden data directory which contains the actual data file(s).
*<p>
* An item may be either private or shared (i.e., versioned) as defined by the
* associated file-system. A shared item utilizes a CheckoutManager and HistoryManager
* for tracking version control data related to this item.
*/
public abstract class LocalFolderItem implements FolderItem {
static final Logger log = LogManager.getLogger(LocalFolderItem.class);
static final String FILE_TYPE = "FILE_TYPE";
static final String READ_ONLY = "READ_ONLY";
static final String CONTENT_TYPE = "CONTENT_TYPE";
static final String CHECKOUT_ID = "CHECKOUT_ID";
static final String EXCLUSIVE_CHECKOUT = "EXCLUSIVE";
static final String CHECKOUT_VERSION = "CHECKOUT_VERSION";
static final String LOCAL_CHECKOUT_VERSION = "LOCAL_CHECKOUT_VERSION";
static final String CONTENT_TYPE_VERSION = "CONTENT_TYPE_VERSION";
static final String DATA_DIR_EXTENSION = ".db";
final PropertyFile propertyFile;
final CheckoutManager checkoutMgr;
final HistoryManager historyMgr;
final LocalFileSystem fileSystem;
final boolean isVersioned;
final boolean useDataDir;
String repositoryName;
long lastModified;
long checkinId = DEFAULT_CHECKOUT_ID;
/**
* Construct an existing item which corresponds to the specified
* property file. If a data directory is found it will be
* associated with this item.
* @param fileSystem file system
* @param propertyFile property file
*/
LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
this.fileSystem = fileSystem;
this.propertyFile = propertyFile;
this.isVersioned = fileSystem.isVersioned();
File dataDir = getDataDir();
this.useDataDir = dataDir.exists();
this.checkoutMgr = null;
this.historyMgr = null;
lastModified = propertyFile.lastModified();
}
/**
* Constructor for a new or existing item which corresponds to the specified
* property file.
* @param fileSystem file system
* @param propertyFile property file
* @param useDataDir if true the getDataDir() method must return an appropriate
* directory for data storage.
* @param create if true the data directory will be created
* @throws IOException
*/
LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean useDataDir,
boolean create) throws IOException {
this.fileSystem = fileSystem;
this.propertyFile = propertyFile;
this.isVersioned = fileSystem.isVersioned();
this.useDataDir = useDataDir || isVersioned;
boolean success = false;
try {
if (create) {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
if (propertyFile.exists()) {
throw new DuplicateFileException(getName() + " already exists.");
}
if (useDataDir) {
File dir = getDataDir();
if (dir.exists()) {
throw new DataDirectoryException("Data directory already exists", dir);
}
if (!dir.mkdir()) {
throw new IOException("Failed to create " + getName());
}
}
propertyFile.writeState();
}
else if ((useDataDir && !getDataDir().exists()) || !propertyFile.exists()) {
throw new FileNotFoundException(getName() + " not found");
}
if (isVersioned) {
checkoutMgr = new CheckoutManager(this, create);
historyMgr = new HistoryManager(this, create);
}
else {
checkoutMgr = null;
historyMgr = null;
}
success = true;
}
finally {
if (!success && create) {
abortCreate();
}
}
lastModified = propertyFile.lastModified();
}
void log(String msg, String user) {
fileSystem.log(this, msg, user);
}
@Override
public LocalFolderItem refresh() throws IOException {
if ((useDataDir && !getDataDir().exists()) || !propertyFile.exists()) {
return null;
}
propertyFile.readState();
return this;
}
/**
* Returns hidden database directory
*/
File getDataDir() {
synchronized (fileSystem) {
// Use hidden DB directory
return new File(propertyFile.getFolder(), LocalFileSystem.HIDDEN_DIR_PREFIX +
LocalFileSystem.escapeHiddenDirPrefixChars(propertyFile.getStorageName()) +
DATA_DIR_EXTENSION);
}
}
/**
* Return the oldest/minimum version.
* @throws IOException thrown if an IO error occurs.
*/
abstract int getMinimumVersion() throws IOException;
/**
* Verify that the specified version of this item is not in use.
* @param version the specific version to check for versioned items.
* @throws FileInUseException
*/
void checkInUse(int version) throws FileInUseException {
synchronized (fileSystem) {
if (checkoutMgr != null) {
boolean isCheckedOut;
try {
isCheckedOut = checkoutMgr.isCheckedOut(version);
}
catch (IOException e) {
throw new FileInUseException(getName() + " versioning error", e);
}
if (isCheckedOut) {
throw new FileInUseException(getName() + " version " + version +
" is checked out");
}
}
else if (!isVersioned && getCheckoutId() != DEFAULT_CHECKOUT_ID) {
throw new FileInUseException(getName() + " is checked out");
}
}
}
/**
* Verify that this item is not in use.
* @throws FileInUseException
*/
void checkInUse() throws FileInUseException {
synchronized (fileSystem) {
if (fileSystem.migrationInProgress()) {
return; // migration not affected by checkouts
}
if (checkoutMgr != null) {
boolean isCheckedOut;
try {
isCheckedOut = checkoutMgr.isCheckedOut();
}
catch (IOException e) {
throw new FileInUseException(getName() + " versioning error", e);
}
if (isCheckedOut) {
throw new FileInUseException(getName() + " is checked out");
}
}
else if (!isVersioned && getCheckoutId() != DEFAULT_CHECKOUT_ID) {
throw new FileInUseException(getName() + " is checked out");
}
}
}
/**
* Begin the check-in process for a versioned item.
* @param checkoutId assigned at time of checkout, becomes the check-in ID.
* @throws FileInUseException
*/
void beginCheckin(long checkoutId) throws FileInUseException {
synchronized (fileSystem) {
if (checkinId != DEFAULT_CHECKOUT_ID) {
ItemCheckoutStatus status;
try {
status = checkoutMgr.getCheckout(checkinId);
}
catch (IOException e) {
throw new FileInUseException(getName() + " versioning error", e);
}
String byMsg = status != null ? (" by: " + status.getUser()) : "";
throw new FileInUseException("Another checkin is in progress" + byMsg);
}
checkinId = checkoutId;
//Log.put("Check-in started: " + checkinId);
}
}
/**
* Terminates a check-in which is in progress or has been completed.
* @param itemCheckinId used to validate termination request.
*/
void endCheckin(long itemCheckinId) {
synchronized (fileSystem) {
if (this.checkinId == itemCheckinId) {
this.checkinId = DEFAULT_CHECKOUT_ID;
//Log.put("Check-in ended: " + checkinId);
}
}
}
/**
* Send out notification this item has just been created.
*/
void fireItemCreated() {
fileSystem.getListener().itemCreated(getParentPath(), getName());
}
/**
* Send out notification that this item has changed in some way.
*/
void fireItemChanged() {
fileSystem.getListener().itemChanged(getParentPath(), getName());
}
/**
* Abort the creation of
*
*/
void abortCreate() {
synchronized (fileSystem) {
propertyFile.delete();
if (useDataDir) {
FileUtilities.deleteDir(getDataDir());
}
}
}
/**
* @see ghidra.framework.store.FolderItem#delete(int, java.lang.String)
*/
@Override
public void delete(int version, String user) throws IOException {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
String parentPath = getParentPath();
String name = getName();
boolean deleted = false;
int currentVersion = getCurrentVersion();
if (version == -1) {
if (isVersioned) {
checkInUse();
}
deleteContent(user);
deleted = true;
}
else if (!isVersioned) {
throw new IllegalArgumentException(
"delete version must be -1 for non-versioned items");
}
else {
int minVersion = getMinimumVersion();
if (version == minVersion) {
checkInUse(version);
if (minVersion == currentVersion) {
deleteContent(user);
deleted = true;
}
else {
deleteMinimumVersion(user);
}
}
else if (version == currentVersion) {
checkInUse(version);
deleteCurrentVersion(user);
}
else {
throw new IOException("Only the oldest or latest version may be deleted");
}
}
if (deleted) {
fileSystem.itemDeleted(parentPath, name); // de-allocates index entry
if (currentVersion > 0) {
// Only notify if initial version was created
fileSystem.getListener().itemDeleted(parentPath, name);
}
fileSystem.deleteEmptyVersionedFolders(getParentPath());
}
else {
fireItemChanged();
}
}
}
/**
* Remove this item from the associated filesystem.
* The property file and the hidden data directory are removed.
* If in-use files prevent removal a FileInUseException will be thrown.
* @param user user performing removal
* @throws IOException if an error occurs during the delete operation.
* Files are restored to there original state if unable to remove
* all files.
*/
void deleteContent(String user) throws IOException {
synchronized (fileSystem) {
File dataDir = getDataDir();
File chkDir = new File(dataDir.getParentFile(), dataDir.getName() + ".delete");
FileUtilities.deleteDir(chkDir);
if (useDataDir && dataDir.exists() && !dataDir.renameTo(chkDir)) {
throw new FileInUseException(getName() + " is in use");
}
boolean success = false;
try {
propertyFile.delete();
if (propertyFile.exists()) {
throw new FileInUseException(getName() + " is in use");
}
success = true;
}
finally {
if (!success) {
if (useDataDir && !dataDir.exists() && chkDir.exists() && propertyFile.exists()) {
chkDir.renameTo(dataDir);
}
}
else {
if (useDataDir) {
FileUtilities.deleteDir(chkDir);
}
log("file deleted", user);
}
}
}
}
/**
* Delete the item content associated with the minimum version.
* This method will only be invoked for versioned items and will
* never be the only version (i.e., minVersion will always be less
* than the currentVersion).
* @param user user name
* @throws IOException
*/
abstract void deleteMinimumVersion(String user) throws IOException;
/**
* Delete the item content associated with the current version.
* This method will only be invoked for versioned items and will
* never be the only version (i.e., minVersion will always be less
* than the currentVersion).
* @param user user name
* @throws IOException
*/
abstract void deleteCurrentVersion(String user) throws IOException;
/**
* Move this item into a newFolder which has a path of newPath.
* @param newFolder new parent directory/folder
* @param newStorageName new storage name
* @param newPath new parent path
* @throws DuplicateFileException
* @throws FileInUseException
* @throws IOException
* @see ghidra.framework.store.FileSystem#moveItem
*/
void moveTo(File newFolder, String newStorageName, String newFolderPath, String newName)
throws IOException {
synchronized (fileSystem) {
checkInUse();
File oldFolder = propertyFile.getFolder();
String oldStorageName = propertyFile.getStorageName();
String oldPath = propertyFile.getParentPath();
File oldDbDir = getDataDir();
String oldName = getName();
propertyFile.moveTo(newFolder, newStorageName, newFolderPath, newName);
boolean success = false;
try {
File newDbDir = getDataDir();
if (useDataDir && !newDbDir.equals(oldDbDir)) {
if (newDbDir.exists()) {
throw new DuplicateFileException(getName() + " already exists");
}
else if (!oldDbDir.renameTo(newDbDir)) {
throw new FileInUseException(getName() + " is in use");
}
}
success = true;
}
finally {
if (!success) {
propertyFile.moveTo(oldFolder, oldStorageName, oldPath, oldName);
}
}
}
}
/**
* @see ghidra.framework.store.FolderItem#getContentType()
*/
@Override
public String getContentType() {
return propertyFile.getString(CONTENT_TYPE, null);
}
/**
* @see ghidra.framework.store.FolderItem#getFileID()
*/
@Override
public String getFileID() {
return propertyFile.getFileID();
}
/**
* @see ghidra.framework.store.FolderItem#resetFileID()
*/
@Override
public String resetFileID() throws IOException {
String fileId = FileIDFactory.createFileID();
String oldFileId = propertyFile.getFileID();
propertyFile.setFileID(fileId);
propertyFile.writeState();
fileSystem.fileIdChanged(propertyFile, oldFileId);
return fileId;
}
/**
* @see ghidra.framework.store.FolderItem#getName()
*/
@Override
public String getName() {
return propertyFile.getName();
}
// /**
// * Change the name of this item's property file and hidden data directory
// * based upon the new item name.
// * If in-use files prevent renaming a FileInUseException will be thrown.
// * @param name new name for this item
// * @throws InvalidNameException invalid name was specified
// * @throws IOException an error occured
// */
// void doSetName(String name) throws InvalidNameException, IOException {
// synchronized (fileSystem) {
// File oldDbDir = getDataDir();
// String oldName = getName();
//
// boolean success = false;
// try {
// propertyFile.setName(name);
// File newDbDir = getDataDir();
// if (useDataDir) {
// if (newDbDir.exists()) {
// throw new DuplicateFileException(getName() + " already exists");
// }
// else if (!oldDbDir.renameTo(newDbDir)) {
// throw new FileInUseException(oldName + " is in use");
// }
// }
// success = true;
// }
// finally {
// if (!success && !propertyFile.getName().equals(oldName)) {
// propertyFile.setName(oldName);
// }
// }
// }
// }
/**
* @see ghidra.framework.store.FolderItem#getParentPath()
*/
@Override
public String getParentPath() {
synchronized (fileSystem) {
return propertyFile.getParentPath();
}
}
/**
* @see ghidra.framework.store.FolderItem#getPathName()
*/
@Override
public String getPathName() {
synchronized (fileSystem) {
return propertyFile.getPath();
}
}
/**
* @see ghidra.framework.store.FolderItem#isCheckedOut()
*/
@Override
public boolean isCheckedOut() {
if (isVersioned) {
throw new UnsupportedOperationException(
"isCheckedOut is not applicable to versioned item");
}
return (getCheckoutId() != DEFAULT_CHECKOUT_ID);
}
@Override
public boolean isCheckedOutExclusive() {
if (isVersioned) {
throw new UnsupportedOperationException(
"isCheckedOutExclusive is not applicable to versioned item");
}
synchronized (fileSystem) {
if (propertyFile.getLong(CHECKOUT_ID, DEFAULT_CHECKOUT_ID) != DEFAULT_CHECKOUT_ID) {
return propertyFile.getBoolean(EXCLUSIVE_CHECKOUT, false);
}
}
return false;
}
/**
* @see ghidra.framework.store.FolderItem#isVersioned()
*/
@Override
public boolean isVersioned() throws IOException {
return fileSystem.isVersioned();
}
/**
* @see ghidra.framework.store.FolderItem#getVersions()
*/
@Override
public synchronized Version[] getVersions() throws IOException {
synchronized (fileSystem) {
if (!isVersioned) {
throw new UnsupportedOperationException(
"Non-versioned item does not support getVersions");
}
return historyMgr.getVersions();
}
}
/**
* @see ghidra.framework.store.FolderItem#lastModified()
*/
@Override
public long lastModified() {
return lastModified;
}
/**
* @see ghidra.framework.store.FolderItem#isReadOnly()
*/
@Override
public boolean isReadOnly() {
return propertyFile.getBoolean(READ_ONLY, false);
}
/**
* @see ghidra.framework.store.FolderItem#setReadOnly(boolean)
*/
@Override
public void setReadOnly(boolean state) throws IOException {
if (isVersioned) {
throw new IOException("Versioned item does not support read-only property");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
synchronized (this) {
propertyFile.putBoolean(READ_ONLY, state);
propertyFile.writeState();
}
fireItemChanged();
}
}
@Override
public int getContentTypeVersion() {
return propertyFile.getInt(CONTENT_TYPE_VERSION, 1);
}
@Override
public void setContentTypeVersion(int version) throws IOException {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
synchronized (this) {
propertyFile.putInt(CONTENT_TYPE_VERSION, version);
propertyFile.writeState();
}
fireItemChanged();
}
}
@Override
public ItemCheckoutStatus checkout(CheckoutType checkoutType, String user, String projectPath)
throws IOException {
if (!isVersioned) {
throw new UnsupportedOperationException("Non-versioned item does not support checkout");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
ItemCheckoutStatus coStatus =
checkoutMgr.newCheckout(checkoutType,
user, getCurrentVersion(), projectPath);
if (checkoutType != CheckoutType.NORMAL && coStatus != null && getFileID() == null) {
// Establish missing fileID for on exclusive checkout
resetFileID();
}
return coStatus;
}
}
@Override
public void terminateCheckout(long checkoutId, boolean notify) throws IOException {
if (!isVersioned) {
throw new UnsupportedOperationException("Non-versioned item does not support checkout");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
ItemCheckoutStatus coStatus = checkoutMgr.getCheckout(checkoutId);
if (coStatus == null) {
throw new IOException("Invalid checkout ID");
}
if (checkoutId == checkinId) {
throw new IOException("Checkin is in-progress");
}
checkoutMgr.endCheckout(checkoutId);
}
if (notify) {
fireItemChanged();
}
}
@Override
public ItemCheckoutStatus getCheckout(long checkoutId) throws IOException {
synchronized (fileSystem) {
if (!isVersioned) {
throw new UnsupportedOperationException(
"Non-versioned item does not support checkout");
}
return checkoutMgr.getCheckout(checkoutId);
}
}
@Override
public ItemCheckoutStatus[] getCheckouts() throws IOException {
synchronized (fileSystem) {
if (!isVersioned) {
throw new UnsupportedOperationException(
"Non-versioned item does not support checkout");
}
return checkoutMgr.getAllCheckouts();
}
}
@Override
public long getCheckoutId() {
synchronized (fileSystem) {
if (isVersioned) {
throw new UnsupportedOperationException(
"getCheckoutId is not applicable to versioned item");
}
return propertyFile.getLong(CHECKOUT_ID, DEFAULT_CHECKOUT_ID);
}
}
@Override
public int getCheckoutVersion() throws IOException {
synchronized (fileSystem) {
if (isVersioned) {
throw new UnsupportedOperationException(
"getCheckoutVersion is not applicable to versioned item");
}
return propertyFile.getInt(CHECKOUT_VERSION, -1);
}
}
@Override
public int getLocalCheckoutVersion() {
synchronized (fileSystem) {
if (isVersioned) {
throw new UnsupportedOperationException(
"getLocalCheckoutVersion is not applicable to versioned item");
}
return propertyFile.getInt(LOCAL_CHECKOUT_VERSION, -1);
}
}
@Override
public void setCheckout(long checkoutId, boolean exclusive, int checkoutVersion,
int localVersion) throws IOException {
if (isVersioned) {
throw new UnsupportedOperationException(
"setCheckout is not applicable to versioned item");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
if (checkoutId <= 0 || checkoutVersion <= 0 || localVersion < 0) {
throw new IllegalArgumentException("Bad checkout data: " + checkoutId + "," +
checkoutVersion + "," + localVersion);
}
propertyFile.putLong(CHECKOUT_ID, checkoutId);
propertyFile.putBoolean(EXCLUSIVE_CHECKOUT, exclusive);
propertyFile.putInt(CHECKOUT_VERSION, checkoutVersion);
propertyFile.putInt(LOCAL_CHECKOUT_VERSION, localVersion);
propertyFile.writeState();
fireItemChanged();
}
}
@Override
public void clearCheckout() throws IOException {
if (isVersioned) {
throw new UnsupportedOperationException(
"clearCheckout is not applicable to versioned item");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
propertyFile.putLong(CHECKOUT_ID, DEFAULT_CHECKOUT_ID);
propertyFile.putBoolean(EXCLUSIVE_CHECKOUT, false);
propertyFile.putInt(CHECKOUT_VERSION, -1);
propertyFile.putInt(LOCAL_CHECKOUT_VERSION, -1);
propertyFile.writeState();
fireItemChanged();
}
}
/**
* Returns the appropriate instantiation of a LocalFolderItem
* based upon a specified property file which resides within a
* LocalFileSystem.
* @param fileSystem local file system which contains property file
* @param propertyFile property file which identifies the folder item.
* @return folder item
*/
static LocalFolderItem getFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
int fileType = propertyFile.getInt(FILE_TYPE, UNKNOWN_FILE_TYPE);
try {
if (fileType == DATAFILE_FILE_TYPE) {
return new LocalDataFile(fileSystem, propertyFile);
}
else if (fileType == DATABASE_FILE_TYPE) {
return new LocalDatabaseItem(fileSystem, propertyFile);
}
}
catch (FileNotFoundException e) {
log.error("Item may be corrupt due to missing file: " + propertyFile.getPath(), e);
}
catch (IOException e) {
log.error("Item may be corrupt: " + propertyFile.getPath(), e);
}
return new UnknownFolderItem(fileSystem, propertyFile);
}
@Override
public boolean hasCheckouts() {
synchronized (fileSystem) {
if (isVersioned) {
try {
return checkoutMgr.isCheckedOut();
}
catch (IOException e) {
Msg.error(getName() + " versioning error", e);
return true;
}
}
return false;
}
}
@Override
public boolean isCheckinActive() {
synchronized (fileSystem) {
if (isVersioned) {
return checkinId != DEFAULT_CHECKOUT_ID;
}
return false;
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof LocalFolderItem) {
return propertyFile.equals(((LocalFolderItem) obj).propertyFile);
}
return false;
}
/**
* Update this non-versioned item with the latest version of the specified versioned item.
* @param versionedFolderItem versioned item which corresponds to this
* non-versioned item.
* @param updateItem if true this items content is updated using the versionedFolderItem
* @param monitor progress monitor for update
* @throws IOException if this file is not a checked-out non-versioned file
* or an IO error occurs.
* @throws CancelledException if monitor cancels operation
*/
public abstract void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
TaskMonitor monitor) throws IOException, CancelledException;
/**
* Update this non-versioned item with the contents of the specified item which must be
* within the same non-versioned fileSystem. If successful, the specified item will be
* removed after its content has been moved into this item.
* @param item
* @param checkoutVersion
* @throws IOException if this file is not a checked-out non-versioned file
* or an IO error occurs.
*/
public abstract void updateCheckout(FolderItem item, int checkoutVersion) throws IOException;
@Override
public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
throws IOException {
if (!isVersioned) {
throw new UnsupportedOperationException(
"updateCheckoutVersion is not applicable to non-versioned item");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
synchronized (fileSystem) {
ItemCheckoutStatus checkout = getCheckout(checkoutId);
if (checkout == null || !checkout.getUser().equals(user)) {
throw new IOException("Checkout not found");
}
checkoutMgr.updateCheckout(checkoutId, checkoutVersion);
}
}
}

View file

@ -0,0 +1,697 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.timer.GTimer;
import ghidra.util.timer.GTimerMonitor;
import java.io.*;
import java.util.Date;
import java.util.Random;
/**
* Provides for the creation and management of a named lock file. Keep in mind
* that if a lock expires it may be removed without notice. Care should be
* taken to renew a lock file in a timely manner.
*
*
*/
public class LockFile {
/**
* The maximum lock lease period in seconds. To retain a
* lock longer than this period of time, the renewLock() method must be
* invoked before the lock expires. This is the amount of time a user will have to
* wait for a stuck lock to be removed.
*/
private static final int DEFAULT_MAX_LOCK_LEASE_PERIOD_MS = 15000; // 15 seconds
private static final int DEFAULT_LOCK_RENEWAL_PERIOD = DEFAULT_MAX_LOCK_LEASE_PERIOD_MS - 2000;
/**
* The default timeout for obtaining a lock.
*/
private static final int DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
private static final int MAX_DELETE_TRIES = 3;
private int maxLockLeasePeriod = DEFAULT_MAX_LOCK_LEASE_PERIOD_MS;
private int lockRenewalPeriod = DEFAULT_LOCK_RENEWAL_PERIOD;
private int lockTimeout = DEFAULT_TIMEOUT_MS;
private static final String LOCK = "lock";
public static int nextInstanceId;
private int instanceId;
private static int debugId = getDebugId();
private static int getDebugId() {
int id = (new Random()).nextInt();
if (id < 0) {
id = -id;
}
return id;
}
private File lockFile;
private long deltaTime = Long.MAX_VALUE;
private Object waitLock = new Object(); // synchronization lock
private GTimerMonitor waitTimerMonitor;
private WaitForLockRunnable waitTask;
private Object holdLock = new Object(); // synchronization lock
private GTimerMonitor holdTimerMonitor;
private int lockCount = 0;
private long myLockTime = 0;
// for testing
LockFile(File dir, String name, int maxLockLeasePeriod, int lockRenewalPeriod, int lockTimeout) {
this(dir, name, "");
this.maxLockLeasePeriod = maxLockLeasePeriod;
this.lockRenewalPeriod = lockRenewalPeriod;
this.lockTimeout = lockTimeout;
}
/**
* Constructor.
* @param dir directory containing lock file
* @param name unmangled name of entity which this lock is associated with.
*/
public LockFile(File dir, String name) {
this(dir, name, "");
}
/**
* Constructor.
* @param dir directory containing lock file
* @param name unmangled name of entity which this lock is associated with.
* @param lockType unique lock identifier (may not contain a '.')
*/
public LockFile(File dir, String name, String lockType) {
if (lockType.indexOf('.') >= 0) {
throw new AssertException("Illegal lockType");
}
lockFile = new File(dir, NamingUtilities.mangle(name) + "." + lockType + LOCK);
instanceId = getNextInstanceId();
Msg.trace(this, "Instantiated lock: " + getLockID());
}
/**
* Constructor.
* @param file file whose lock state will be controlled with this lock file.
*/
public LockFile(File file) {
lockFile = new File(file.getParentFile(), file.getName() + "." + LOCK);
instanceId = getNextInstanceId();
Msg.trace(this, "Instantiated lock: " + getLockID());
}
/**
* @param dir directory containing lock file
* @param mangledName mangled name of file or entity which this lock is associated with.
* @return true if any lock exists within dir for the given entity name.
*/
private static boolean hasAnyLock(File dir, final String mangledName) {
FileFilter filter = new FileFilter() {
@Override
public boolean accept(File pathname) {
String fname = pathname.getName();
if (fname.startsWith(mangledName) && fname.endsWith(LOCK)) {
String s =
fname.substring(mangledName.length(), fname.length() - LOCK.length());
return s.indexOf('.') == 0 && s.indexOf('.', 1) < 0;
}
return false;
}
};
File[] files = dir.listFiles(filter);
return files != null && files.length != 0;
}
/**
* @param dir directory containing lock file
* @param name of entity which this lock is associated with.
* @return true if any lock exists within dir for the given entity name.
*/
public static boolean isLocked(File dir, String name) {
return hasAnyLock(dir, NamingUtilities.mangle(name));
}
/**
* @param file file whose lock state is controlled with this lock file.
* @return true if any lock exists within dir for the given entity name.
*/
public static boolean isLocked(File file) {
return hasAnyLock(file.getParentFile(), file.getName());
}
public static boolean containsLock(File dir) {
File[] files = dir.listFiles();
if (files == null)
return false;
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
if (containsLock(files[i]))
return true;
}
else if (files[i].getName().endsWith(LOCK)) {
return true;
}
}
return false;
}
private static synchronized int getNextInstanceId() {
return nextInstanceId++;
}
private static synchronized int getNextDebugId() {
return ++debugId;
}
/**
* Determine if lock file was successfully created by this instance.
* This does not quarentee that the lock is still present if more
* than MAX_LOCK_LEASE_PERIOD has lapsed since lock was created.
* @return true if lock has been created, otherwise false.
*/
public boolean haveLock() {
return lockCount != 0;
}
/**
* Determine if lock is still in place.
* Verifying the lock may be necessary when slow processes are holding
* the lock without timely renewals.
* @return true if lock is still in place, otherwise false.
*/
public boolean haveLock(boolean verify) {
if (lockCount != 0) {
if (!verify || (myLockTime == lockFile.lastModified())) {
return true;
}
Msg.trace(this, "lock was stolen : " + getLockID());
lockCount = 0;
}
return false;
}
/**
* Renew the lease on a lock.
* This is accomplished by changing its last modification time.
* @return true if lock extension granted, else false.
*/
private boolean renewLock() {
if (haveLock(true) && setLockOwner()) {
//
// File.setLastModified fails to work properly on Linux (JDK 1.4.2).
// Lock file content re-written to update time
//
myLockTime = lockFile.lastModified();
return true;
}
return false;
}
/**
* Return the name of the current lock owner
* or "<Unknown>" if not locked or could not be determined.
*/
public String getLockOwner() {
return getLockOwner(false);
}
private String getLockOwner(boolean includeId) {
String owner = null;
FileInputStream fin = null;
try {
fin = new FileInputStream(lockFile);
byte[] bytes = new byte[32];
int cnt = fin.read(bytes);
owner = new String(bytes, 0, cnt);
if (!includeId) {
int spaceIndex = owner.indexOf(' ');
if (spaceIndex > 0) {
owner = owner.substring(0, spaceIndex);
}
}
}
catch (Exception e) {
owner = "<Unknown>";
}
finally {
if (fin != null) {
try {
fin.close();
}
catch (IOException e1) {
// we tried
}
}
}
return owner;
}
private boolean setLockOwner() {
Msg.trace(this, "writing lock data : " + getLockID());
BufferedOutputStream fout = null;
boolean success = false;
try {
fout = new BufferedOutputStream(new FileOutputStream(lockFile, false));
fout.write((SystemUtilities.getUserName() + " " + getNextDebugId()).getBytes());
success = true;
}
catch (Exception e) {
// we will check 'success' later
}
finally {
if (fout != null) {
try {
fout.close();
}
catch (IOException e1) {
// we tried
}
}
if (!success) {
lockFile.delete();
}
}
return success;
}
private String getLockID() {
return lockFile.getName() + "(" + instanceId + "," + Thread.currentThread().getName() + ")";
}
@Override
public String toString() {
return getLockID();
}
/**
* Remove the lock file.
* This method should be invoked when the corresponding transaction is complete.
*/
public synchronized void removeLock() {
if (haveLock(true)) {
if (--lockCount == 0) {
holdLock(false);
Msg.trace(this, "removing lock : " + getLockID());
int tryCnt = MAX_DELETE_TRIES;
while ((tryCnt-- > 0) && !lockFile.delete()) {
Msg.warn(this, "Failed to remove lock file : " + getLockID());
}
}
else {
Msg.trace(this, "lock count reduced (" + lockCount + "): " + getLockID());
}
}
else {
Msg.trace(this, "attempted to remove lock which I do not own " + getLockOwner(true) +
": " + getLockID());
try {
throw new AssertException("Lock time = " + lockFile.lastModified());
}
catch (Exception e) {
Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
}
}
}
/**
* Create the lock file using the default timeout.
* Lock is guaranteed for MAX_LOCK_LEASE_PERIOD seconds.
* @param timeout maximum time in seconds to wait for lock.
* @return true if lock creation was successful.
*/
public boolean createLock() {
return createLock(lockTimeout, false);
}
/**
* Create the lock file.
* If another lock file already exists, wait for it to expire
* within the specified timeout period. Method will block
* until either the lock is obtained or the timeout period lapses.
* @param timeout maximum time in milliseconds to wait for lock.
* @param hold if true the lock will be held and maintained until
* removed, otherwise it is only guaranteed for MAX_LOCK_LEASE_PERIOD seconds.
* @return true if lock creation was successful.
*/
public synchronized boolean createLock(int timeout, boolean hold) {
synchronized (waitLock) {
// Renew lock if we already have it
if (lockCount != 0 && renewLock()) {
++lockCount;
Msg.trace(this, "increased lock count (" + lockCount + "): " + getLockID());
if (hold)
holdLock(true);
return true;
}
// Check if we can get lock immediately
try {
if (createLockFileNoWait(true)) {
if (waitTask != null) {
waitTask.abort = true;
}
++lockCount;
Msg.trace(this, "increased lock count (" + lockCount + "): " + getLockID());
if (hold)
holdLock(true);
return true;
}
}
catch (IOException e) {
Msg.showError(this, null, "Lock Failure", "Unable to write to lock file: " +
lockFile.getAbsolutePath(), e);
return false;
}
Msg.trace(this, "wait for lock...: " + getLockID());
// Start lock wait/cleanup task
startWaitTimer(timeout != 0);
if (timeout == 0)
return false;
}
// Wait for waitTask
if (waitTask != null && timeout > 0) {
synchronized (waitTask) {
try {
waitTask.wait(timeout);
}
catch (InterruptedException e) {
return false;
}
}
}
synchronized (waitLock) {
// Don't create lock if we timed-out
if (waitTask != null) {
synchronized (waitTask) {
waitTask.create = false;
}
}
// Hold lock if requested
if (lockCount != 0) {
if (hold)
holdLock(true);
return true;
}
Msg.trace(this, "failed to obtain lock...: " + getLockID());
return false;
}
}
/**
* Attempt once to create a lock file.
* @param testLock if true an expiration check will be performed on the lock
* @return true if the lock file successfully created, else false.
*/
private boolean createLockFileNoWait(boolean testLock) throws IOException {
Msg.trace(this, "attempt lock creation...: " + getLockID());
boolean lockCreated = lockFile.createNewFile();
if (!lockCreated && testLock) {
long ltime = lockFile.lastModified();
if (ltime != 0) {
if (deltaTime == Long.MAX_VALUE) {
// Compute time delta between filesystem and my clock
File testFile = File.createTempFile("test", ".tmp", lockFile.getParentFile());
deltaTime = (new Date().getTime()) - testFile.lastModified();
testFile.delete();
}
// Check for expired lock
if (ltime < (new Date().getTime() - maxLockLeasePeriod - deltaTime)) {
lockFile.delete();
Msg.warn(this, "Forcefully removing lock owned by " + getLockOwner(true) +
": " + getLockID());
lockCreated = lockFile.createNewFile();
}
}
else {
lockCreated = lockFile.createNewFile();
}
}
if (!lockCreated || !setLockOwner()) {
Msg.trace(this, "lock denied by " + getLockOwner(true) + ": " + getLockID());
return false;
}
Msg.trace(this, "lock created (" + debugId + "): " + getLockID());
myLockTime = lockFile.lastModified();
return true;
}
/**
* Start the wait task if it is not already running.
* Set the create flag within the wait task.
* @param create an attempt to create a lock file will be done if true,
* otherwise only attempt to remove stale lock file.
*/
private void startWaitTimer(boolean create) {
synchronized (waitLock) {
if (waitTask == null) {
waitTask = new WaitForLockRunnable(create, 1000);
waitTimerMonitor = GTimer.scheduleRepeatingRunnable(500, 1000, waitTask);
}
else {
waitTask.create = create;
}
}
}
/**
* Cancel the current wait timer.
*/
private void endWaitTimer() {
waitTimerMonitor.cancel();
waitTimerMonitor = null;
waitTask = null;
}
/**
* Provides a runnable class which waits for a lock to be removed.
* If the lock expires while waiting, the lock file is removed.
* No attempt should be made to create the lock file while this
* task is running.
*/
private class WaitForLockRunnable implements Runnable {
private int interval;
private boolean create;
private long lastModTime;
private int maxLeaseTime;
private boolean abort = false;
/**
* Constructor.
* @param create if true an attempt will be made to create the lock
* if the current lock is removed.
* @param interval time period in milliseconds which the run method is invoked.
*/
WaitForLockRunnable(boolean create, int interval) {
this.interval = interval;
this.create = create;
maxLeaseTime = maxLockLeasePeriod;
lastModTime = lockFile.lastModified();
}
private synchronized void terminate() {
endWaitTimer();
notifyAll();
}
/**
* Check to see if the current lock file has exceeded the
* maximum allowed lease time.
*/
@Override
public void run() {
synchronized (waitLock) {
if (abort) {
terminate();
return;
}
maxLeaseTime -= interval;
long mt = lockFile.lastModified();
if (mt != 0L) {
// Check for updated lock
if (mt != lastModTime) {
// Discontinue waiting if we are not trying to create lock
// Since it is clearly not stuck
if (!create) {
terminate();
}
// Reset lease timer if we want to create lock
else {
maxLeaseTime = maxLockLeasePeriod;
lastModTime = mt;
Msg.trace(this, getLockOwner(true) + " grabbed lock before I could: " +
getLockID());
}
return; // lock file still exists
}
Msg.trace(this, getLockOwner(true) + " has held lock for " +
((maxLockLeasePeriod - maxLeaseTime) / 1000) + " seconds: " + getLockID());
if (maxLeaseTime > 0)
return;
// Forcefully remove lock file if max lease time expired
lockFile.delete();
Msg.warn(this, "Forcefully removing lock owned by " + getLockOwner(true) +
": " + getLockID());
// Delay after forceful removal to avoid race condition!
// If we create a new lock file immediately, another wait task
// could remove it due to the delay between checking the lastModified
// time and actually removing the file.
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
create = false;
}
}
// Attempt to create lock if requested
if (create) {
try {
if (createLockFileNoWait(false)) {
Msg.trace(this, (new Date()) + " LockFile: lock granted after wait: " +
getLockID());
++lockCount;
terminate();
}
else {
// create failed - keep waiting
maxLeaseTime = maxLockLeasePeriod;
return;
}
}
catch (IOException e) {
Msg.showError(this, null, "Lock Failure", "Unable to write to lock file: " +
lockFile.getAbsolutePath(), e);
terminate();
}
}
lastModTime = 0L;
}
}
}
/**
* Initiate lock hold.
* Lock will continue to be renewed until holdLockThread is interrupted.
*/
private void holdLock(boolean hold) {
synchronized (holdLock) {
if (holdTimerMonitor != null) {
if (!hold) {
holdTimerMonitor.cancel();
holdTimerMonitor = null;
}
}
else if (hold) {
holdTimerMonitor =
GTimer.scheduleRepeatingRunnable(lockRenewalPeriod, lockRenewalPeriod,
new HoldLockRunnable());
}
}
}
private class HoldLockRunnable implements Runnable {
@Override
public void run() {
synchronized (holdLock) {
if (holdTimerMonitor == null) {
// we were cancelled while waiting for the 'holdLock' lock
return;
}
if (!renewLock()) {
// We lost lock hold for some reason
holdTimerMonitor.cancel();
holdTimerMonitor = null;
}
}
}
}
/**
* Cleanup lock resources and tasks.
* Invoking this method could prevent stale locks from being removed
* if createLock was invoked with a very short timeout.
* Use of dispose is optional - the associated wait task should
* stop by it self allowing the LockFile object to be finalized.
*/
public synchronized void dispose() {
holdLock(false);
if (waitTimerMonitor != null) {
waitTimerMonitor.cancel();
waitTimerMonitor = null;
waitTask = null;
}
if (lockCount != 0) {
removeLock();
}
}
/**
* Cleanup during garbage collection.
*/
@Override
protected void finalize() {
dispose();
}
}

View file

@ -0,0 +1,487 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.*;
import ghidra.util.exception.DuplicateFileException;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import utilities.util.FileUtilities;
/**
* <code>MangledLocalFileSystem</code> implements the legacy project data storage
* scheme which utilizes a simplified name mangling which provides case-sensitive
* file-naming with support for spaces. Project folder hierarchy maps directly to
* the actual storage hierarchy.
*/
public class MangledLocalFileSystem extends LocalFileSystem {
public static final int MAX_NAME_LENGTH = 60; // allow room for name mangling
private boolean migrationInProgress = false;
/**
* Constructor.
* @param rootPath path for root directory (must already exist).
* @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException
* @param enableAsyncronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed.
* @throws FileNotFoundException if specified rootPath does not exist
*/
MangledLocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
boolean enableAsyncronousDispatching) throws FileNotFoundException {
super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching);
if (!readOnly) {
cleanupAfterConstruction();
}
}
/**
* Constructor for an empty read-only file-system.
*/
MangledLocalFileSystem() {
super();
}
@Override
public int getMaxNameLength() {
return MAX_NAME_LENGTH;
}
/**
* Find an existing storage location
* @param folderPath
* @param itemName
* @return storage location. A non-null value does not guarantee that the associated
* item actually exists.
* @throws FileNotFoundException
*/
@Override
protected ItemStorage findItemStorage(String folderPath, String itemName)
throws FileNotFoundException {
File dir = getFile(folderPath);
String storageName = mangleName(itemName);
return new ItemStorage(dir, storageName, folderPath, itemName);
}
/**
* Allocate a new storage location
* @param folderPath
* @param itemName
* @return storage location
* @throws DuplicateFileException if item path has previously been allocated
* @throws IOException if invalid path/item name specified
* @throws InvalidNameException if folderPath or itemName contains invalid characters
*/
@Override
protected ItemStorage allocateItemStorage(String folderPath, String itemName)
throws IOException, InvalidNameException {
ItemStorage itemStorage = findItemStorage(folderPath, itemName);
File pf = new File(itemStorage.dir, itemStorage.storageName + PROPERTY_EXT);
if (pf.exists()) {
throw new DuplicateFileException(getPath(folderPath, itemName) + " already exists.");
}
createFolders(itemStorage.dir, folderPath);
return itemStorage;
}
/**
* Deallocate item storage
* @param folderPath
* @param itemName
*/
@Override
protected void deallocateItemStorage(String folderPath, String itemName) {
// nothing to do for mangled name allocation
}
@Override
public int getItemCount() {
throw new UnsupportedOperationException("getItemCount");
}
// private int getItemCount(File dir) {
// int count = 0;
// for (File f : dir.listFiles()) {
// String name = f.getName();
//
// if (f.isDirectory()) {
// if (name.startsWith(HIDDEN_DIR_PREFIX)) {
// continue;
// }
// count += getItemCount(f);
// }
// else if (name.endsWith(PROPERTY_EXT)) {
// ++count;
// }
// }
// return count;
// }
@Override
protected String[] getItemNames(String folderPath, boolean includeHiddenFiles)
throws IOException {
File dir = getFile(folderPath);
File[] dirList = dir.listFiles();
if (dirList == null) {
throw new FileNotFoundException("Folder " + folderPath + " not found");
}
ArrayList<String> fileList = new ArrayList<String>(dirList.length);
for (int i = 0; i < dirList.length; i++) {
String name = dirList[i].getName();
if (name.endsWith(PROPERTY_EXT) && dirList[i].isFile()) {
if (!NamingUtilities.isValidMangledName(dirList[i].getName())) {
log.warn("Ignoring property file with bad name: " + dirList[i]);
continue;
}
int index = name.lastIndexOf(PROPERTY_EXT);
name = demangleName(name.substring(0, index));
if (name != null && (includeHiddenFiles || !name.startsWith(HIDDEN_ITEM_PREFIX))) {
fileList.add(name);
}
}
}
Collections.sort(fileList);
return fileList.toArray(new String[fileList.size()]);
}
/*
* @see ghidra.framework.store.FileSystem#getFolders(java.lang.String)
*/
public synchronized String[] getFolderNames(String folderPath) throws IOException {
File dir = getFile(folderPath);
File[] dirList = dir.listFiles();
if (dirList == null) {
throw new FileNotFoundException("Folder " + folderPath + " not found");
}
ArrayList<String> folderList = new ArrayList<String>(dirList.length);
for (int i = 0; i < dirList.length; i++) {
if (!dirList[i].isDirectory()) {
continue;
}
String name = demangleName(dirList[i].getName());
if (name != null) {
folderList.add(name);
}
}
Collections.sort(folderList);
return folderList.toArray(new String[folderList.size()]);
}
/*
* @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String)
*/
public synchronized void createFolder(String parentPath, String folderName)
throws InvalidNameException, IOException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(parentPath, true);
testValidName(folderName, false);
String path = getPath(parentPath, folderName);
File dir = getFile(path);
if (dir.exists()) {
return; // ignore request if already exists
}
createFolders(dir, path);
}
/*
* @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String)
*/
public synchronized void deleteFolder(String folderPath) throws IOException {
if (readOnly) {
throw new ReadOnlyException();
}
if (SEPARATOR.equals(folderPath)) {
throw new IOException("Root folder may not be deleted");
}
File file = getFile(folderPath);
if (!file.exists()) {
return;
}
if (!file.isDirectory()) {
throw new FileNotFoundException(folderPath + " does not exist or is not a directory");
}
String[] contents = file.list();
if (contents.length != 0) {
if (contents.length > 1 || !".properties".equals(contents[0])) {
throw new FolderNotEmptyException(folderPath + " is not empty");
}
}
FileUtilities.deleteDir(file);
listeners.folderDeleted(getParentPath(folderPath), getName(folderPath));
}
/*
* @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String)
*/
public synchronized void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException {
boolean success = false;
try {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(newParentPath, true);
String folderPath = getPath(parentPath, folderName);
File folder = getFile(folderPath);
if (!folder.isDirectory()) {
throw new FileNotFoundException(folderPath + " does not exist or is not a folder");
}
// TODO Must scan for items in use !!!
String newFolderPath = getPath(newParentPath, folderName);
File newFolder = getFile(newFolderPath);
if (newFolder.exists()) {
throw new DuplicateFileException(newFolderPath + " already exists.");
}
createFolders(getFile(newParentPath), newParentPath);
if (!folder.renameTo(newFolder)) {
throw new IOException("move failed for unknown reason");
}
listeners.folderMoved(parentPath, folderName, newParentPath);
deleteEmptyVersionedFolders(parentPath);
}
finally {
if (!success) {
deleteEmptyVersionedFolders(newParentPath);
}
}
}
/*
* @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String)
*/
public synchronized void renameFolder(String parentPath, String folderName, String newFolderName)
throws InvalidNameException, IOException {
if (readOnly) {
throw new ReadOnlyException();
}
testValidName(newFolderName, false);
String folderPath = getPath(parentPath, folderName);
File folder = getFile(folderPath);
if (!folder.isDirectory()) {
throw new FileNotFoundException(folderPath + " does not exist or is not a folder");
}
String newFolderPath = getPath(parentPath, newFolderName);
File newFolder = getFile(newFolderPath);
if (newFolder.exists()) {
throw new DuplicateFileException(newFolderPath + " already exists.");
}
// TODO Must scan for items in use !!!
if (!folder.renameTo(newFolder)) {
throw new IOException("Folder may contain files that are in use");
}
listeners.folderRenamed(parentPath, folderName, newFolderName);
}
/**
* Returns the underlying File object which corresponds to the specified unmangled path.
* @param path unmangled path for folder or file
* @return File object
* @throws FileNotFoundException if specified file path does not begin with '/'
*/
private File getFile(String path) throws FileNotFoundException {
if (root == null) {
throw new FileNotFoundException("Empty read-only file system");
}
if (path.charAt(0) != SEPARATOR_CHAR) {
throw new FileNotFoundException("Path names must begin with \'" + SEPARATOR_CHAR + "\'");
}
if (path.length() == 1) {
return root;
}
path = toSystemDependantSeparator(manglePath(path));
return new File(root, path);
}
private String manglePath(String path) {
if (SEPARATOR.equals(path)) {
return path;
}
StringBuilder buf = new StringBuilder();
String[] split = path.split(SEPARATOR);
for (int i = 0; i < split.length; i++) {
buf.append(SEPARATOR_CHAR);
buf.append(escapeHiddenDirPrefixChars(split[i]));
}
return NamingUtilities.mangle(buf.toString());
}
/**
* Mangle non-hidden name
* @param name
* @return mangled non-hidden name
*/
private String mangleName(String name) {
return NamingUtilities.mangle(escapeHiddenDirPrefixChars(name));
}
/**
* Demangle non-hidden name
* @param name
* @return demangled name or null if name was hidden
*/
private String demangleName(String name) {
// null will be returned if this is used on a hidden name
return unescapeHiddenDirPrefixChars(NamingUtilities.demangle(name));
}
/**
* Convert the path separators to system specific File path separators.
* @param path file path
* @return modified file path
*/
private String toSystemDependantSeparator(String path) {
int n = path.length();
StringBuffer sb = new StringBuffer(n - 1);
for (int i = 1; i < n; i++) {
char c = path.charAt(i);
c = (c == SEPARATOR_CHAR) ? File.separatorChar : c;
sb.append(c);
}
return sb.toString();
}
/**
* Recursively create all directories associated with the specified
* folder path.
* @param folderDir folder path
*/
private void createFolders(File folderDir, String folderPath) throws FileNotFoundException {
if (folderDir.exists()) {
return;
}
File parentDir = folderDir.getParentFile();
String parentPath = getParentPath(folderPath);
createFolders(parentDir, parentPath);
folderDir.mkdir();
listeners.folderCreated(parentPath, getName(folderPath));
}
/*
* @see ghidra.framework.store.FileSystem#folderExists(java.lang.String)
*/
@Override
public boolean folderExists(String folderPath) {
try {
File file = getFile(folderPath);
return file.isDirectory();
}
catch (FileNotFoundException e) {
return false;
}
}
@Override
public boolean migrationInProgress() {
return migrationInProgress;
}
/**
* Convert this mangled filesystem to an indexed filesystem. This instance should be discarded
* and not used once the conversion has completed.
*
* @throws IOException
*/
public synchronized void convertToIndexedLocalFileSystem() throws IOException {
if (readOnly) {
throw new IOException("Unable to convert read-only filesystem");
}
cleanupAfterConstruction(); // remove all temporary content
File tmpRoot =
new File(root.getCanonicalFile().getParentFile(), HIDDEN_DIR_PREFIX + '.' +
root.getName());
if (tmpRoot.exists() || !tmpRoot.mkdir()) {
throw new IOException("Failed to create data directory: " + tmpRoot);
}
IndexedV1LocalFileSystem indexedFs =
new IndexedV1LocalFileSystem(tmpRoot.getAbsolutePath(), isVersioned, false, false, true);
migrationInProgress = true;
migrateFolder(SEPARATOR, indexedFs);
indexedFs.dispose();
for (File f : root.listFiles()) {
File newFile = new File(tmpRoot, f.getName());
f.renameTo(newFile);
}
if (!root.delete()) {
throw new IOException("Failed to remove old root following conversion: " + root);
}
tmpRoot.renameTo(root);
}
private void migrateFolder(String folderPath, IndexedLocalFileSystem indexedFs)
throws IOException {
try {
for (String name : getFolderNames(folderPath)) {
indexedFs.createFolder(folderPath, name);
migrateFolder(getPath(folderPath, name), indexedFs);
}
for (String name : getItemNames(folderPath)) {
LocalFolderItem item = getItem(folderPath, name);
indexedFs.migrateItem(item);
}
if (!SEPARATOR.equals(folderPath)) {
// non-root should be empty - remove it
File dir = getFile(folderPath);
dir.delete();
}
}
catch (InvalidNameException e) {
throw new IOException("Unexpected exception", e);
}
}
}

View file

@ -0,0 +1,23 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.store.local;
public interface RepositoryLogger {
void log(String path, String msg, String user);
}

View file

@ -0,0 +1,176 @@
/* ###
* 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.store.local;
import java.io.File;
import java.io.IOException;
import ghidra.framework.store.*;
import ghidra.util.PropertyFile;
import ghidra.util.task.TaskMonitor;
/**
* <code>UnknownFolderItem</code> acts as a LocalFolderItem place-holder for
* items of an unknown type.
*/
public class UnknownFolderItem extends LocalFolderItem {
public static final String UNKNOWN_CONTENT_TYPE = "Unknown";
/**
* Constructor.
* @param fileSystem local file system
* @param propertyFile property file associated with this item
*/
UnknownFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
super(fileSystem, propertyFile);
}
@Override
public long length() throws IOException {
return 0;
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, boolean, ghidra.util.task.TaskMonitor)
*/
@Override
public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException();
}
/*
* @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, int)
*/
@Override
public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException {
throw new UnsupportedOperationException();
}
/*
* @see ghidra.framework.store.FolderItem#checkout(java.lang.String)
*/
public synchronized ItemCheckoutStatus checkout(String user) throws IOException {
throw new IOException(propertyFile.getName() +
" may not be checked-out, item may be corrupt");
}
/*
* @see ghidra.framework.store.FolderItem#terminateCheckout(long)
*/
public synchronized void terminateCheckout(long checkoutId) {
// Do nothing
}
/*
* @see ghidra.framework.store.FolderItem#clearCheckout()
*/
@Override
public void clearCheckout() throws IOException {
// Do nothing
}
/*
* @see ghidra.framework.store.FolderItem#setCheckout(long, int, int)
*/
public void setCheckout(long checkoutId, int checkoutVersion, int localVersion) {
// Do nothing
}
/*
* @see ghidra.framework.store.FolderItem#getCheckout(long)
*/
@Override
public synchronized ItemCheckoutStatus getCheckout(long checkoutId) throws IOException {
return null;
}
/*
* @see ghidra.framework.store.FolderItem#getCheckouts()
*/
@Override
public synchronized ItemCheckoutStatus[] getCheckouts() throws IOException {
return new ItemCheckoutStatus[0];
}
/*
* @see ghidra.framework.store.FolderItem#getVersions()
*/
@Override
public synchronized Version[] getVersions() throws IOException {
throw new IOException("History data is unavailable for " + propertyFile.getName());
}
/*
* @see ghidra.framework.store.FolderItem#getContentType()
*/
@Override
public String getContentType() {
return UNKNOWN_CONTENT_TYPE;
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteMinimumVersion(java.lang.String)
*/
@Override
void deleteMinimumVersion(String user) throws IOException {
throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems");
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#deleteCurrentVersion(java.lang.String)
*/
@Override
void deleteCurrentVersion(String user) throws IOException {
throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems");
}
/*
* @see ghidra.framework.store.FolderItem#output(java.io.File, int, ghidra.util.task.TaskMonitor)
*/
public void output(File outputFile, int version, TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException("Output not supported for UnknownFolderItems");
}
/*
* @see ghidra.framework.store.local.LocalFolderItem#getMinimumVersion()
*/
@Override
int getMinimumVersion() throws IOException {
return -1;
}
/*
* @see ghidra.framework.store.FolderItem#getCurrentVersion()
*/
public int getCurrentVersion() {
return -1;
}
/*
* @see ghidra.framework.store.FolderItem#canRecover()
*/
public boolean canRecover() {
return false;
}
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body>
Provides low-level storage implementations based upon directories and files
stored within the local file-system.
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body>
Provides common interfaces and classes used for low-level storage based upon an abstraction of a
file-system. &nbsp;The storage mechanism is implementation specific.<br>
</body>
</html>

View file

@ -0,0 +1,134 @@
/* ###
* 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.store.remote;
import java.io.*;
import db.buffers.*;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.RepositoryItem;
import ghidra.framework.store.DatabaseItem;
import ghidra.framework.store.local.ItemSerializer;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>RemoteDatabaseItem</code> provides a FolderItem implementation
* for a remote database. This item wraps an underlying versioned database
* which corresponds to a repository item.
*/
public class RemoteDatabaseItem extends RemoteFolderItem implements DatabaseItem {
/**
* Construct a FolderItem for an existing repository database item.
* @param repository repository which contains item
* @param item repository item
*/
RemoteDatabaseItem(RepositoryAdapter repository, RepositoryItem item) {
super(repository, item);
}
@Override
public long length() throws IOException {
return repository.getLength(parentPath, itemName);
}
@Override
int getItemType() {
return RepositoryItem.DATABASE;
}
@Override
public boolean canRecover() {
return false;
}
@Override
public ManagedBufferFileAdapter open(int version, int minChangeDataVer) throws IOException {
return repository.openDatabase(parentPath, itemName, version, minChangeDataVer);
}
@Override
public ManagedBufferFileAdapter open(int version) throws IOException {
return repository.openDatabase(parentPath, itemName, version, -1);
}
@Override
public ManagedBufferFileAdapter open() throws IOException {
return repository.openDatabase(parentPath, itemName, LATEST_VERSION, -1);
}
@Override
public ManagedBufferFileAdapter openForUpdate(long checkoutId) throws IOException {
return repository.openDatabase(parentPath, itemName, checkoutId);
}
@Override
public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
throws IOException {
repository.updateCheckoutVersion(parentPath, itemName, checkoutId, checkoutVersion);
}
/*
* @see ghidra.framework.store.FolderItem#hasCheckouts()
*/
@Override
public boolean hasCheckouts() throws IOException {
return repository.hasCheckouts(parentPath, itemName);
}
@Override
public boolean isCheckinActive() throws IOException {
return repository.isCheckinActive(parentPath, itemName);
}
@Override
public void output(File outputFile, int version, TaskMonitor monitor)
throws IOException, CancelledException {
BufferFile bf = repository.openDatabase(parentPath, itemName, version, -1);
try {
File tmpFile = File.createTempFile("ghidra", LocalBufferFile.TEMP_FILE_EXT);
tmpFile.delete();
BufferFile tmpBf = new LocalBufferFile(tmpFile, bf.getBufferSize());
try {
LocalBufferFile.copyFile(bf, tmpBf, null, monitor);
tmpBf.close();
InputStream itemIn = new FileInputStream(tmpFile);
try {
ItemSerializer.outputItem(getName(), getContentType(), DATABASE_FILE_TYPE,
tmpFile.length(), itemIn, outputFile, monitor);
}
finally {
try {
itemIn.close();
}
catch (IOException e) {
}
}
}
finally {
tmpBf.close();
tmpFile.delete();
}
}
finally {
bf.close();
}
}
}

View file

@ -0,0 +1,243 @@
/* ###
* 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.store.remote;
import java.io.*;
import db.buffers.*;
import ghidra.framework.client.RemoteAdapterListener;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.RepositoryItem;
import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>RemoteFileSystem</code> provides access to versioned FolderItem's which
* exist within a Repository-based directory structure. FolderItem
* caching is provided by the remote implementation which is intended
* to be shared across multiple clients.
* <p>
* FolderItem's must be checked-out to create new versions.
* <p>
* FileSystemListener's will be notified of all changes made
* within the Repository.
*/
public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
private RepositoryAdapter repository;
private FileSystemListenerList listeners = new FileSystemListenerList(true);
/**
* Construct a new remote file system which corresponds to a remote repository.
* @param repository remote Repository
* @throws IOException
*/
public RemoteFileSystem(RepositoryAdapter repository) {
this.repository = repository;
repository.setFileSystemListener(listeners);
repository.addListener(this);
}
@Override
public String getUserName() {
try {
return repository.getUser().getName();
}
catch (IOException e) {
return null;
}
}
@Override
public void addFileSystemListener(FileSystemListener listener) {
listeners.add(listener);
}
@Override
public void removeFileSystemListener(FileSystemListener listener) {
listeners.remove(listener);
}
@Override
public boolean isVersioned() {
return true;
}
@Override
public boolean isOnline() {
return repository.isConnected();
}
@Override
public boolean isReadOnly() throws IOException {
return repository.getUser().isReadOnly();
}
@Override
public boolean isShared() {
return true;
}
@Override
public int getItemCount() throws IOException, UnsupportedOperationException {
return repository.getItemCount();
}
@Override
public synchronized String[] getItemNames(String folderPath) throws IOException {
RepositoryItem[] items = repository.getItemList(folderPath);
String[] names = new String[items.length];
for (int i = 0; i < items.length; i++) {
names[i] = items[i].getName();
}
return names;
}
@Override
public synchronized FolderItem getItem(String folderPath, String name) throws IOException {
RepositoryItem item = repository.getItem(folderPath, name);
if (item == null) {
return null;
}
if (item.getItemType() == RepositoryItem.DATABASE) {
return new RemoteDatabaseItem(repository, item);
}
throw new IOException("Unsupported file type");
}
@Override
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
RepositoryItem item = repository.getItem(fileID);
if (item == null) {
return null;
}
if (item.getItemType() == RepositoryItem.DATABASE) {
return new RemoteDatabaseItem(repository, item);
}
throw new IOException("Unsupported file type");
}
@Override
public String[] getFolderNames(String parentPath) throws IOException {
return repository.getSubfolderList(parentPath);
}
@Override
public void createFolder(String parentPath, String folderName) {
throw new UnsupportedOperationException();
}
@Override
public ManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath)
throws InvalidNameException, IOException {
return repository.createDatabase(parentPath, name, bufferSize, contentType, fileID,
projectPath);
}
@Override
public DatabaseItem createDatabase(String parentPath, String name, String fileID,
BufferFile bufferFile, String comment, String contentType, boolean resetDatabaseId,
TaskMonitor monitor, String user)
throws InvalidNameException, IOException, CancelledException {
ManagedBufferFile newFile = repository.createDatabase(parentPath, name,
bufferFile.getBufferSize(), contentType, fileID, null);
boolean success = false;
try {
newFile.setVersionComment(comment);
LocalBufferFile.copyFile(bufferFile, newFile, null, monitor);
long checkinId = newFile.getCheckinID();
newFile.close();
repository.terminateCheckout(parentPath, name, checkinId, false);
success = true;
}
finally {
if (!success) {
newFile.delete();
}
newFile.dispose();
}
return (DatabaseItem) getItem(parentPath, name);
}
@Override
public DataFileItem createDataFile(String parentPath, String name, InputStream istream,
String comment, String contentType, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
repository.createDataFile(parentPath, name);
return (DataFileItem) getItem(parentPath, name);
}
@Override
public FolderItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user)
throws InvalidNameException, IOException, CancelledException {
throw new UnsupportedOperationException("Versioned filesystem does not support createFile");
}
@Override
public void deleteFolder(String folderPath) throws IOException {
throw new UnsupportedOperationException(
"Versioned filesystem does not support deleteFolder");
}
@Override
public void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException {
repository.moveFolder(parentPath, newParentPath, folderName, folderName);
}
@Override
public void renameFolder(String parentPath, String folderName, String newFolderName)
throws InvalidNameException, IOException {
repository.moveFolder(parentPath, parentPath, folderName, newFolderName);
}
@Override
public void moveItem(String parentPath, String name, String newParentPath, String newName)
throws InvalidNameException, IOException {
repository.moveItem(parentPath, newParentPath, name, newName);
}
@Override
public boolean folderExists(String folderPath) throws IOException {
return repository.folderExists(folderPath);
}
@Override
public boolean fileExists(String folderPath, String itemName) throws IOException {
return repository.fileExists(folderPath, itemName);
}
@Override
public void connectionStateChanged(Object adapter) {
if (adapter == repository) {
listeners.syncronize();
}
}
@Override
public void dispose() {
listeners.dispose();
}
}

View file

@ -0,0 +1,270 @@
/* ###
* 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.store.remote;
import java.io.IOException;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.RepositoryItem;
import ghidra.framework.store.*;
/**
* <code>RemoteFolderItem</code> provides an abstract FolderItem implementation
* for an item contained within a remote Repository.
*/
public abstract class RemoteFolderItem implements FolderItem {
protected String parentPath;
protected String itemName;
protected String contentType;
protected int version;
protected long versionTime;
protected RepositoryAdapter repository;
/**
* Construct a FolderItem for an existing repository item.
* @param repository repository which contains item
* @param item repository item
*/
RemoteFolderItem(RepositoryAdapter repository, RepositoryItem item) {
this.repository = repository;
parentPath = item.getParentPath();
itemName = item.getName();
contentType = item.getContentType();
version = item.getVersion();
versionTime = item.getVersionTime();
}
/**
* Returns the item type as defined by RepositoryItem.
* @see ghidra.framework.remote.RepositoryItem
*/
abstract int getItemType();
/*
* @see ghidra.framework.store.FolderItem#getName()
*/
public String getName() {
return itemName;
}
/*
* @see ghidra.framework.store.FolderItem#refresh()
*/
public RemoteFolderItem refresh() throws IOException {
RepositoryItem item = repository.getItem(parentPath, itemName);
if (item == null) {
return null;
}
version = item.getVersion();
versionTime = item.getVersionTime();
return this;
}
/**
* @throws IOException
* @see ghidra.framework.store.FolderItem#getFileID()
*/
public String getFileID() throws IOException {
RepositoryItem item = repository.getItem(parentPath, itemName);
if (item != null) {
return item.getFileID();
}
return null;
}
/**
* @see ghidra.framework.store.FolderItem#resetFileID()
*/
public String resetFileID() {
throw new UnsupportedOperationException();
}
/*
* @see ghidra.framework.store.FolderItem#getContentType()
*/
public String getContentType() {
return contentType;
}
/*
* @see ghidra.framework.store.FolderItem#getParentPath()
*/
public String getParentPath() {
return parentPath;
}
/*
* @see ghidra.framework.store.FolderItem#getPathName()
*/
public String getPathName() {
String path = parentPath;
if (path.length() != 1) {
path += FileSystem.SEPARATOR;
}
return path + itemName;
}
/*
* @see ghidra.framework.store.FolderItem#isReadOnly()
*/
public boolean isReadOnly() {
throw new UnsupportedOperationException("isReadOnly is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#setReadOnly(boolean)
*/
public void setReadOnly(boolean state) {
throw new UnsupportedOperationException("setReadOnly is not applicable to versioned item");
}
/**
* Returns the version of content type. Note this is the version of the structure/storage
* for the content type, Not the users version of their data.
*/
public int getContentTypeVersion() {
throw new UnsupportedOperationException(
"getContentTypeVersion is not applicable to versioned item");
}
/**
* @see ghidra.framework.store.FolderItem#setContentTypeVersion(int)
*/
public void setContentTypeVersion(int version) throws IOException {
throw new UnsupportedOperationException(
"setContentTypeVersion is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#lastModified()
*/
public long lastModified() {
return versionTime;
}
/*
* @see ghidra.framework.store.FolderItem#getCurrentVersion()
*/
public int getCurrentVersion() {
return version;
}
/*
* @see ghidra.framework.store.FolderItem#isVersioned()
*/
public boolean isVersioned() {
return (version != -1);
}
/*
* @see ghidra.framework.store.FolderItem#getVersions()
*/
public Version[] getVersions() throws IOException {
return repository.getVersions(parentPath, itemName);
}
/*
* @see ghidra.framework.store.FolderItem#delete(int, java.lang.String)
*/
public void delete(int ver, String user) throws IOException {
repository.deleteItem(parentPath, itemName, ver);
}
/*
* @see ghidra.framework.store.FolderItem#isPrivate()
*/
public boolean isCheckedOut() {
throw new UnsupportedOperationException("isCheckedOut is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#isCheckedOutExclusive()
*/
public boolean isCheckedOutExclusive() {
throw new UnsupportedOperationException(
"isCheckedOutExclusive is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#checkout(boolean, java.lang.String, java.lang.String)
*/
public ItemCheckoutStatus checkout(CheckoutType checkoutType, String user, String projectPath)
throws IOException {
return repository.checkout(parentPath, itemName, checkoutType, projectPath);
}
/*
* @see ghidra.framework.store.FolderItem#terminateCheckout(long, boolean)
*/
public void terminateCheckout(long checkoutId, boolean notify) throws IOException {
repository.terminateCheckout(parentPath, itemName, checkoutId, notify);
}
/*
* @see ghidra.framework.store.FolderItem#getCheckout(long)
*/
public ItemCheckoutStatus getCheckout(long checkoutId) throws IOException {
return repository.getCheckout(parentPath, itemName, checkoutId);
}
/*
* @see ghidra.framework.store.FolderItem#getCheckouts()
*/
public ItemCheckoutStatus[] getCheckouts() throws IOException {
return repository.getCheckouts(parentPath, itemName);
}
/*
* @see ghidra.framework.store.FolderItem#clearCheckout()
*/
public void clearCheckout() throws IOException {
throw new UnsupportedOperationException("clearCheckout is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#getCheckoutId()
*/
public long getCheckoutId() throws IOException {
throw new UnsupportedOperationException("getCheckoutId is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#getCheckoutVersion()
*/
public int getCheckoutVersion() throws IOException {
throw new UnsupportedOperationException(
"getCheckoutVersion is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#getLocalCheckoutVersion()
*/
public int getLocalCheckoutVersion() {
throw new UnsupportedOperationException(
"getLocalCheckoutVersion is not applicable to versioned item");
}
/*
* @see ghidra.framework.store.FolderItem#setCheckout(long, boolean, int, int)
*/
public void setCheckout(long checkoutId, boolean exclusive, int checkoutVersion,
int localVersion) throws IOException {
throw new UnsupportedOperationException("setCheckout is not applicable to versioned item");
}
}

View file

@ -0,0 +1,201 @@
/* ###
* 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.util;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class HashUtilities {
public static String MD5_ALGORITHM = "MD5";
public static String SHA256_ALGORITHM = "SHA-256";
public static final int SALT_LENGTH = 4;
public static final int MD5_UNSALTED_HASH_LENGTH = 32;
public static final int MD5_SALTED_HASH_LENGTH = MD5_UNSALTED_HASH_LENGTH + SALT_LENGTH;
public static final int SHA256_UNSALTED_HASH_LENGTH = 64;
public static final int SHA256_SALTED_HASH_LENGTH = SHA256_UNSALTED_HASH_LENGTH + SALT_LENGTH;
private static Random random = new Random(System.nanoTime());
static char getRandomLetterOrDigit() {
int val = (random.nextInt() % 62); // 0-9,A-Z,a-z (10+26+26=62)
if (val < 0) {
val = -val;
}
int c = '0' + val;
if (c > '9') {
c = 'A' + (val - 10);
}
if (c > 'Z') {
c = 'a' + (val - 36);
}
return (char) c;
}
/**
* Generate hash in a hex character representation
* @param algorithm message digest algorithm
* @param msg message text
* @return hex hash value in text format
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported algorithms
*/
public static char[] getHash(String algorithm, char[] msg) {
return getSaltedHash(algorithm, new char[0], msg);
}
/**
* Generate salted hash for specified message. Supplied salt is
* returned as prefix to returned hash.
* @param algorithm message digest algorithm
* @param salt digest salt (use empty string for no salt)
* @param msg message text
* @return salted hash using specified salt which is
* returned as a prefix to the hash
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported hash algorithms
*/
public static char[] getSaltedHash(String algorithm, char[] salt, char[] msg) {
MessageDigest messageDigest = getMessageDigest(algorithm);
byte[] msgBytes = new byte[msg.length + salt.length];
try {
for (int i = 0; i < salt.length; i++) {
msgBytes[i] = (byte) salt[i];
}
for (int i = 0; i < msg.length; i++) {
msgBytes[i + salt.length] = (byte) msg[i];
}
char[] hash = hexDump(messageDigest.digest(msgBytes));
char[] saltedHash = new char[hash.length + SALT_LENGTH];
System.arraycopy(salt, 0, saltedHash, 0, salt.length);
System.arraycopy(hash, 0, saltedHash, salt.length, hash.length);
return saltedHash;
}
finally {
// attempt to remove message from memory since it may be a password.
Arrays.fill(msgBytes, (byte) 0);
}
}
/**
* Generate salted hash for specified message using random salt.
* First 4-characters of returned hash correspond to the salt data.
* @param algorithm message digest algorithm
* @param msg message text
* @return salted hash using randomly generated salt which is
* returned as a prefix to the hash
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported hash algorithms
*/
public static char[] getSaltedHash(String algorithm, char[] msg) {
char[] salt = new char[SALT_LENGTH];
for (int i = 0; i < SALT_LENGTH; i++) {
salt[i] = getRandomLetterOrDigit();
}
return getSaltedHash(algorithm, salt, msg);
}
/**
* Generate message digest hash for specified input stream. Stream will be read
* until EOF is reached.
* @param algorithm message digest algorithm
* @param in input stream
* @return message digest hash
* @throws IOException if reading input stream produces an error
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported hash algorithms
*/
public static String getHash(String algorithm, InputStream in) throws IOException {
MessageDigest messageDigest = getMessageDigest(algorithm);
byte[] buf = new byte[16 * 1024];
int cnt;
while ((cnt = in.read(buf)) >= 0) {
messageDigest.update(buf, 0, cnt);
}
return NumericUtilities.convertBytesToString(messageDigest.digest());
}
/**
* Generate message digest hash for specified file contents.
* @param algorithm message digest algorithm
* @param file file to be read
* @return message digest hash
* @throws IOException if opening or reading file produces an error
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported hash algorithms
*/
public static String getHash(String algorithm, File file) throws IOException {
try (FileInputStream in = new FileInputStream(file)) {
return getHash(algorithm, in);
}
}
/**
* Generate combined message digest hash for all values in the
* specified values list.
* @param algorithm message digest algorithm
* @param values list of text strings
* @return message digest hash
* @throws IllegalArgumentException if specified algorithm is not supported
* @see MessageDigest for supported hash algorithms
*/
public static String getHash(String algorithm, List<String> values) {
MessageDigest messageDigest = getMessageDigest(algorithm);
for (String value : values) {
if (value != null) {
messageDigest.update(value.getBytes());
}
}
return NumericUtilities.convertBytesToString(messageDigest.digest());
}
private static MessageDigest getMessageDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Algorithm not supported: " + algorithm);
}
}
/**
* Convert binary data to a sequence of hex characters.
* @param data binary data
* @return hex character representation of data
*/
public static char[] hexDump(byte[] data) {
char buf[] = new char[data.length * 2];
for (int i = 0; i < data.length; i++) {
String b = Integer.toHexString(data[i] & 0xFF);
if (b.length() < 2) {
buf[i * 2 + 0] = '0';
buf[i * 2 + 1] = b.charAt(0);
}
else {
buf[i * 2 + 0] = b.charAt(0);
buf[i * 2 + 1] = b.charAt(1);
}
}
return buf;
}
}

View file

@ -0,0 +1,103 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util;
import java.io.*;
import java.util.List;
public class MD5Utilities {
public static final int SALT_LENGTH = HashUtilities.SALT_LENGTH;
public static final int UNSALTED_HASH_LENGTH = HashUtilities.MD5_UNSALTED_HASH_LENGTH;
public static final int SALTED_HASH_LENGTH = HashUtilities.MD5_SALTED_HASH_LENGTH;
/**
* Generate MD5 hash in a hex character representation
* @param msg message text
* @return hex hash value in text format
*/
public static char[] getMD5Hash(char[] msg) {
return HashUtilities.getSaltedHash(HashUtilities.MD5_ALGORITHM, new char[0], msg);
}
/**
* Generate salted MD5 hash for specified message. Supplied salt is
* returned as prefix to returned hash.
* @param salt digest salt (use empty string for no salt)
* @param msg message text
* @return salted hash using specified salt which is
* returned as a prefix to the hash
*/
public static char[] getSaltedMD5Hash(char[] salt, char[] msg) {
return HashUtilities.getSaltedHash(HashUtilities.MD5_ALGORITHM, salt, msg);
}
/**
* Generate salted MD5 hash for specified message using random salt.
* First 4-characters of returned hash correspond to the salt data.
* @param msg message text
* @return salted hash using randomly generated salt which is
* returned as a prefix to the hash
*/
public static char[] getSaltedMD5Hash(char[] msg) {
char[] salt = new char[HashUtilities.SALT_LENGTH];
for (int i = 0; i < salt.length; i++) {
salt[i] = HashUtilities.getRandomLetterOrDigit();
}
return getSaltedMD5Hash(salt, msg);
}
/**
* Generate MD5 message digest hash for specified input stream.
* Stream will be read until EOF is reached.
* @param in input stream
* @return message digest hash
* @throws IOException if reading input stream produces an error
*/
public static String getMD5Hash(InputStream in) throws IOException {
return HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, in);
}
/**
* Generate MD5 message digest hash for specified file contents.
* @param file file to be read
* @return message digest hash
* @throws IOException if opening or reading file produces an error
*/
public static String getMD5Hash(File file) throws IOException {
return HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, file);
}
/**
* Generate combined MD5 message digest hash for all values in the
* specified values list.
* @param values list of text strings
* @return MD5 message digest hash
*/
public static String getMD5Hash(List<String> values) {
return HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, values);
}
/**
* Convert binary data to a sequence of hex characters.
* @param data binary data
* @return hex character representation of data
*/
public static char[] hexDump(byte[] data) {
return HashUtilities.hexDump(data);
}
}

View file

@ -0,0 +1,166 @@
/* ###
* 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.util;
import java.util.Set;
import ghidra.framework.store.FileSystem;
import util.CollectionUtils;
/**
* Utility class with static methods for validating names and converting
* strings to numbers, etc.
*/
public final class NamingUtilities {
/**
* Max length for a name.
*/
public final static int MAX_NAME_LENGTH = 60;
private final static char MANGLE_CHAR = '_';
private final static Set<Character> VALID_NAME_SET = CollectionUtils.asSet('.', '-', ' ', '_');
private NamingUtilities() {
}
/**
* tests whether the given string is a valid name.
* @param name name to validate
*/
public static boolean isValidName(String name) {
if (name == null) {
return false;
}
if (name.indexOf(FileSystem.SEPARATOR_CHAR) >= 0) {
return false;
}
if ((name.length() < 1) || (name.length() > MAX_NAME_LENGTH)) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!Character.isLetterOrDigit(c) && !VALID_NAME_SET.contains(c)) {
return false;
}
}
return true;
}
/**
* Find the invalid character in the given name.
* <p>
* This method should only be used with {@link #isValidName(String)}} and <b>not</b>
* {@link #isValidFileName(String);
*
* @param name the name with an invalid character
* @return the invalid character or 0 if no invalid character can be found
* @see #isValidName(String)
*/
public static char findInvalidChar(String name) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!Character.isLetterOrDigit(c) && !VALID_NAME_SET.contains(c)) {
return c;
}
}
return (char) 0;
}
/**
* Returns a string such that all uppercase characters in the given string are
* replaced by the MANGLE_CHAR followed by the lowercase version of the character.
* MANGLE_CHARs are replaced by 2 MANGLE_CHARs.
*
* This method is to get around the STUPID windows problem where filenames are
* not case sensitive. Under Windows, Foo.exe and foo.exe represent
* the same filename. To fix this we mangle names first such that Foo.exe becomes
* _foo.exe.
*/
public static String mangle(String name) {
int len = name.length();
StringBuffer buf = new StringBuffer(2 * len);
for (int i = 0; i < len; i++) {
char c = name.charAt(i);
if (c == MANGLE_CHAR) {
buf.append(MANGLE_CHAR);
buf.append(MANGLE_CHAR);
}
else if (Character.isUpperCase(c)) {
buf.append(MANGLE_CHAR);
buf.append(Character.toLowerCase(c));
}
else {
buf.append(c);
}
}
return buf.toString();
}
/**
* Performs the inverse of the mangle method. A string is returned such that
* all characters following a MANGLE_CHAR are converted to uppercase. Two MANGLE
* chars in a row are replace by a single MANGLE_CHAR.
*/
public static String demangle(String name) {
int len = name.length();
StringBuffer buf = new StringBuffer(len);
boolean foundMangle = false;
for (int i = 0; i < len; i++) {
char c = name.charAt(i);
if (foundMangle) {
foundMangle = false;
if (c == MANGLE_CHAR) {
buf.append(c);
}
else {
buf.append(Character.toUpperCase(c));
}
}
else if (c == MANGLE_CHAR) {
foundMangle = true;
}
else {
buf.append(c);
}
}
return buf.toString();
}
/**
* Performs a validity check on a mangled name
* @param name mangled name
* @return true if name can be demangled else false
*/
public static boolean isValidMangledName(String name) {
int len = name.length();
for (int i = 0; i < len; i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,41 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.util;
import ghidra.util.exception.UsrException;
/**
* Exception thrown if user is not the owner of a file or
* data object being accessed.
*/
public class NotOwnerException extends UsrException {
/**
* Default constructor.
*/
public NotOwnerException() {
super("User is not the owner");
}
/**
* Constructor
* @param msg detailed message explaining exception.
*/
public NotOwnerException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,441 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.util;
import generic.stl.Pair;
import ghidra.framework.store.FileSystem;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.xml.XmlUtilities;
import ghidra.xml.NonThreadedXmlPullParserImpl;
import ghidra.xml.XmlElement;
import java.io.*;
import java.util.HashMap;
import java.util.Map.Entry;
import org.xml.sax.*;
/**
* Class that represents a file of property names and values. The file
* extension used is PROPERTY_EXT.
*
*/
public class PropertyFile {
/**
* File extension indicating the file is a property file.
*/
public final static String PROPERTY_EXT = ".prp";
private static final String FILE_ID = "FILE_ID";
protected File propertyFile;
protected String storageName;
protected String parentPath;
protected String name;
private static enum PropertyEntryType {
INT_TYPE("int"), LONG_TYPE("long"), BOOLEAN_TYPE("boolean"), STRING_TYPE("string");
PropertyEntryType(String rep) {
this.rep = rep;
}
private final String rep;
public static PropertyEntryType lookup(String rep) {
for (PropertyEntryType entryType : PropertyEntryType.values()) {
if (rep.equals(entryType.rep)) {
return entryType;
}
}
return null;
}
}
private HashMap<String, Pair<PropertyEntryType, String>> map =
new HashMap<String, Pair<PropertyEntryType, String>>();
/**
* Construct a new or existing PropertyFile.
* This form ignores retained property values for NAME and PARENT path.
* @param dir parent directory
* @param storageName stored property file name (without extension)
* @param parentPath path to parent
* @param name name of the property file
* @throws IOException
*/
public PropertyFile(File dir, String storageName, String parentPath, String name)
throws IOException {
if (!dir.isAbsolute()) {
throw new IllegalArgumentException("dir must be specified by an absolute path");
}
this.name = name;
this.parentPath = parentPath;
this.storageName = storageName;
propertyFile = new File(dir, storageName + PROPERTY_EXT);
if (propertyFile.exists()) {
readState();
}
}
/**
* Return the name of this PropertyFile. A null value may be returned
* if this is an older property file and the name was not specified at
* time of construction.
*/
public String getName() {
return name;
}
/**
* Returns true if file is writable
*/
public boolean isReadOnly() {
return !propertyFile.canWrite();
}
/**
* Return the path to this PropertyFile. A null value may be returned
* if this is an older property file and the name and parentPath was not specified at
* time of construction.
*/
public String getPath() {
if (parentPath == null || name == null) {
return null;
}
if (parentPath.length() == 1) {
return parentPath + name;
}
return parentPath + FileSystem.SEPARATOR_CHAR + name;
}
/**
* Return the path to the parent of this PropertyFile.
*/
public String getParentPath() {
return parentPath;
}
/**
* Return the parent file to this PropertyFile.
*/
public File getFolder() {
return propertyFile.getParentFile();
}
/**
* Return the storage name of this PropertyFile. This name does not include the property
* file extension (.prp)
*/
public String getStorageName() {
return storageName;
}
/**
* Returns the FileID associated with this file.
* @returns FileID associated with this file
*/
public String getFileID() {
return getString(FILE_ID, null);
}
/**
* Set the FileID associated with this file.
* @param fileId
*/
public void setFileID(String fileId) {
putString(FILE_ID, fileId);
}
/**
* Return the int value with the given propertyName.
* @param propertyName name of property that is an int
* @param defaultValue value to use if the property does not exist
* @return int value
*/
public int getInt(String propertyName, int defaultValue) {
Pair<PropertyEntryType, String> pair = map.get(propertyName);
if (pair == null || pair.first != PropertyEntryType.INT_TYPE) {
return defaultValue;
}
try {
String value = pair.second;
return Integer.parseInt(value);
}
catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Assign the int value to the given propertyName.
* @param propertyName name of property to set
* @param value value to set
*/
public void putInt(String propertyName, int value) {
map.put(propertyName, new Pair<PropertyEntryType, String>(PropertyEntryType.INT_TYPE,
Integer.toString(value)));
}
/**
* Return the long value with the given propertyName.
* @param propertyName name of property that is a long
* @param defaultValue value to use if the property does not exist
* @return long value
*/
public long getLong(String propertyName, long defaultValue) {
Pair<PropertyEntryType, String> pair = map.get(propertyName);
if (pair == null || pair.first != PropertyEntryType.LONG_TYPE) {
return defaultValue;
}
try {
String value = pair.second;
return Long.parseLong(value);
}
catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Assign the long value to the given propertyName.
* @param propertyName name of property to set
* @param value value to set
*/
public void putLong(String propertyName, long value) {
map.put(propertyName,
new Pair<PropertyEntryType, String>(PropertyEntryType.LONG_TYPE, Long.toString(value)));
}
/**
* Return the string value with the given propertyName.
* @param propertyName name of property that is a string
* @param defaultValue value to use if the property does not exist
* @return string value
*/
public String getString(String propertyName, String defaultValue) {
Pair<PropertyEntryType, String> pair = map.get(propertyName);
if (pair == null || pair.first != PropertyEntryType.STRING_TYPE) {
return defaultValue;
}
String value = pair.second;
return value;
}
/**
* Assign the string value to the given propertyName.
* @param propertyName name of property to set
* @param value value to set
*/
public void putString(String propertyName, String value) {
map.put(propertyName, new Pair<PropertyEntryType, String>(PropertyEntryType.STRING_TYPE,
value));
}
/**
* Return the boolean value with the given propertyName.
* @param propertyName name of property that is a boolean
* @param defaultValue value to use if the property does not exist
* @return boolean value
*/
public boolean getBoolean(String propertyName, boolean defaultValue) {
Pair<PropertyEntryType, String> pair = map.get(propertyName);
if (pair == null || pair.first != PropertyEntryType.BOOLEAN_TYPE) {
return defaultValue;
}
String value = pair.second;
return Boolean.parseBoolean(value);
}
/**
* Assign the boolean value to the given propertyName.
* @param propertyName name of property to set
* @param value value to set
*/
public void putBoolean(String propertyName, boolean value) {
map.put(propertyName, new Pair<PropertyEntryType, String>(PropertyEntryType.BOOLEAN_TYPE,
Boolean.toString(value)));
}
/**
* Remove the specified property
* @param propertyName
*/
public void remove(String propertyName) {
map.remove(propertyName);
}
/**
* Return the time of last modification in number of milliseconds.
*/
public long lastModified() {
return propertyFile.lastModified();
}
/**
* Write the contents of this PropertyFile.
* @throws IOException thrown if there was a problem writing the file
*/
public void writeState() throws IOException {
PrintWriter writer = new PrintWriter(propertyFile);
try {
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
writer.println("<FILE_INFO>");
writer.println(" <BASIC_INFO>");
for (Entry<String, Pair<PropertyEntryType, String>> entry : map.entrySet()) {
String propertyName = entry.getKey();
String propertyType = entry.getValue().first.rep;
String propertyValue = entry.getValue().second;
writer.print(" <STATE NAME=\"");
writer.print(XmlUtilities.escapeElementEntities(propertyName));
writer.print("\" TYPE=\"");
writer.print(XmlUtilities.escapeElementEntities(propertyType));
writer.print("\" VALUE=\"");
writer.print(XmlUtilities.escapeElementEntities(propertyValue));
writer.println("\" />");
}
writer.println(" </BASIC_INFO>");
writer.println("</FILE_INFO>");
}
finally {
writer.close();
}
}
private static final ErrorHandler HANDLER = new ErrorHandler() {
@Override
public void warning(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
};
/**
* Read in this PropertyFile into a SaveState object.
* @throws IOException thrown if there was a problem reading the file
*/
public void readState() throws IOException {
NonThreadedXmlPullParserImpl parser = null;
try {
parser = new NonThreadedXmlPullParserImpl(propertyFile, HANDLER, false);
XmlElement file_info = parser.start("FILE_INFO");
XmlElement basic_info = parser.start("BASIC_INFO");
XmlElement state;
while ((state = parser.softStart("STATE")) != null) {
String propertyName = state.getAttribute("NAME");
String propertyTypeString = state.getAttribute("TYPE");
String propertyValue = state.getAttribute("VALUE");
PropertyEntryType propertyType = PropertyEntryType.lookup(propertyTypeString);
map.put(propertyName, new Pair<PropertyEntryType, String>(propertyType,
propertyValue));
parser.end(state);
}
parser.end(basic_info);
parser.end(file_info);
}
catch (Exception e) {
Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
throw new InvalidObjectException("XML parse error in properties file");
}
finally {
if (parser != null) {
parser.dispose();
}
}
}
/**
* Move this PropertyFile to the newParent file.
* @param newParent new parent of the file
* @param newStorageName new storage name
* @param newParentPath parent path of the new parent
* @param newName new name for this PropertyFile
* @throws IOException thrown if there was a problem accessing the
* @throws DuplicateFileException thrown if a file with the newName
* already exists
*/
public void moveTo(File newParent, String newStorageName, String newParentPath, String newName)
throws DuplicateFileException, IOException {
if (!newParent.equals(propertyFile.getParentFile()) || !newStorageName.equals(storageName)) {
File newPropertyFile = new File(newParent, newStorageName + PROPERTY_EXT);
if (newPropertyFile.exists()) {
throw new DuplicateFileException(newName + " already exists");
}
if (!propertyFile.renameTo(newPropertyFile)) {
throw new IOException("move failed");
}
propertyFile = newPropertyFile;
storageName = newStorageName;
}
parentPath = newParentPath;
name = newName;
}
/**
* Return whether the file for this PropertyFile exists.
*/
public boolean exists() {
return propertyFile.exists();
}
/**
* Delete the file for this PropertyFile.
*/
public void delete() {
propertyFile.delete();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((propertyFile == null) ? 0 : propertyFile.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
PropertyFile other = (PropertyFile) obj;
if (propertyFile == null) {
if (other.propertyFile != null) {
return false;
}
}
else if (!propertyFile.equals(other.propertyFile)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,45 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.util.exception;
import java.io.IOException;
/**
* Exception thrown when a user requests some operation to be performed
* but does not have sufficient privileges.
*
*
*/
public class UserAccessException extends IOException {
/**
* Default constructor.
*/
public UserAccessException() {
super("User has insufficient privilege for operation.");
}
/**
* Create a new UserAccessException with the given message.
*
* @param msg the message explaining what caused the exception.
*/
public UserAccessException(String msg) {
super(msg);
}
}

View file

@ -0,0 +1,443 @@
/* ###
* 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 db.buffers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import org.junit.*;
import db.DBFileListener;
import db.Database;
import generic.test.AbstractGenericTest;
import ghidra.framework.store.db.PrivateDatabase;
import ghidra.util.task.TaskMonitorAdapter;
import utilities.util.FileUtilities;
/**
*
*/
public class RecoveryFileTest extends AbstractGenericTest {
private static int BUFFER_SIZE = LocalBufferFile.getRecommendedBufferSize(500);
private static final File testDir = new File(AbstractGenericTest.getTestDirectoryPath(), "test");
/**
* Constructor for RecoveryFileTest.
* @param arg0
*/
public RecoveryFileTest() {
super();
}
@Before
public void setUp() throws Exception {
FileUtilities.deleteDir(testDir);
testDir.mkdir();
}
@After
public void tearDown() throws Exception {
FileUtilities.deleteDir(testDir);
}
/**
*
* File created:
* bufferCnt buffers are filled with data, -bufferID stored at offset 0
*
* Recovery snapshot taken after each transaction.
* Transaction 1:
* growCnt buffers are added with data, -bufferID stored at offset 0
* Transaction 2:
* all odd numbered buffers between 1 and bufferCnt-1 are deleted/freed
* +bufferID stored at offset 0 for buffers 0, 40, 80 ... <bufferCnt-1>
* Transaction 3:
* +bufferID stored at offset 0 for buffers 20, 60, 100 ... <bufferCnt-1>
* Transaction 4:
* +bufferID stored at offset 0 for buffers 10, 30, 50 ... <bufferCnt-1>
*
* @param bufferCnt
* @param growCnt
* @return
* @throws Exception
*/
private BufferMgr init(int bufferCnt, int growCnt) throws Exception {
BufferMgr bufferMgr = null;
boolean success = false;
try {
LocalBufferFile bf = PrivateDatabase.createDatabase(testDir, new DBFileListener() {
@Override
public void versionCreated(Database db, int version) {
}
}, BUFFER_SIZE);
bufferMgr = new BufferMgr(BUFFER_SIZE, 16 * 1024, 4);
// Add data buffers to original file
for (int i = 0; i < bufferCnt; i++) {
DataBuffer buf = bufferMgr.createBuffer();
buf.putInt(0, -i); // store negative index value
bufferMgr.releaseBuffer(buf);
}
bufferMgr.saveAs(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
// Grow file if requested
int modCnt = 0;
int newBufferCnt = bufferCnt + growCnt;
for (int i = bufferCnt; i < newBufferCnt; i++) {
DataBuffer buf = bufferMgr.createBuffer();
buf.putInt(0, -i); // store negative index value
bufferMgr.releaseBuffer(buf);
++modCnt;
}
bufferCnt = newBufferCnt;
System.out.println("Added " + modCnt + " buffers");
bufferMgr.checkpoint();
bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(RecoveryMgr.canRecover(bf));
// Remove every other buffer (build-up free list)
modCnt = 0;
for (int i = 1; i < bufferCnt; i += 2) {
bufferMgr.deleteBuffer(i);
++modCnt;
}
System.out.println("Deleted " + modCnt + " buffers");
modCnt = 0;
for (int i = 0; i < bufferCnt; i += 40) {
DataBuffer buf = bufferMgr.getBuffer(i);
buf.putInt(0, i); // store positive index value
bufferMgr.releaseBuffer(buf);
++modCnt;
}
System.out.println("Modified " + modCnt + " buffers");
bufferMgr.checkpoint();
bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(RecoveryMgr.canRecover(bf));
modCnt = 0;
for (int i = 20; i < bufferCnt; i += 40) {
DataBuffer buf = bufferMgr.getBuffer(i);
buf.putInt(0, i); // store positive index value
bufferMgr.releaseBuffer(buf);
++modCnt;
}
System.out.println("Modified " + modCnt + " buffers");
bufferMgr.checkpoint();
assertTrue(bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(RecoveryMgr.canRecover(bf));
modCnt = 0;
for (int i = 10; i < bufferCnt; i += 20) {
DataBuffer buf = bufferMgr.getBuffer(i);
buf.putInt(0, i); // store positive index value
bufferMgr.releaseBuffer(buf);
++modCnt;
}
System.out.println("Modified " + modCnt + " buffers");
bufferMgr.checkpoint();
assertTrue(bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(RecoveryMgr.canRecover(bf));
success = true;
}
finally {
if (!success && bufferMgr != null) {
bufferMgr.dispose();
}
}
return bufferMgr;
}
@Test
public void testRecovery() throws Exception {
BufferMgr bufferMgr = null;
BufferMgr bufferMgr2 = null;
try {
// Make sure we stress the RecoveryFile storage
int bufferCnt = BUFFER_SIZE * 10;
bufferMgr = init(bufferCnt, BUFFER_SIZE);
bufferCnt += BUFFER_SIZE;
PrivateDatabase pdb = new PrivateDatabase(testDir);
pdb.refresh();
assertTrue(pdb.canRecover());
// Leave first file open so that recovery files are not removed
// Open a new instance to verify recovery
LocalBufferFile bf2 = pdb.openBufferFileForUpdate();
assertTrue(RecoveryMgr.canRecover(bf2));
bufferMgr2 = new BufferMgr(bf2);
assertTrue(bufferMgr2.recover(TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(bufferMgr2.canSave());
assertEquals(bufferCnt,
bufferMgr2.getAllocatedBufferCount() + bufferMgr2.getFreeBufferCount());
for (int i = 1; i < bufferCnt; i += 2) {
try {
bufferMgr2.getBuffer(i);
Assert.fail("Expected deleted buffer: " + i);
}
catch (IOException e) {
// Ignore
}
}
for (int i = 0; i < bufferCnt; i += 10) {
DataBuffer buf = bufferMgr2.getBuffer(i);
assertEquals(buf.getInt(0), i);
bufferMgr2.releaseBuffer(buf);
}
}
finally {
if (bufferMgr != null) {
bufferMgr.dispose();
}
if (bufferMgr2 != null) {
bufferMgr2.dispose();
}
}
}
@Test
public void testRecoveryWithSave() throws Exception {
BufferMgr bufferMgr = null;
BufferMgr bufferMgr2 = null;
BufferMgr bufferMgr3 = null;
try {
// Make sure we stress the RecoveryFile storage
int bufferCnt = BUFFER_SIZE * 10;
bufferMgr = init(bufferCnt, BUFFER_SIZE);
bufferCnt += BUFFER_SIZE;
PrivateDatabase pdb = new PrivateDatabase(testDir);
pdb.refresh();
assertTrue(pdb.canRecover());
// Leave first file open so that recovery files are not removed
// Open a new instance to verify recovery
LocalBufferFile bf2 = pdb.openBufferFileForUpdate();
assertTrue(RecoveryMgr.canRecover(bf2));
bufferMgr2 = new BufferMgr(bf2);
assertTrue(bufferMgr2.recover(TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(bufferMgr2.canSave());
bufferMgr2.save(null, null, TaskMonitorAdapter.DUMMY_MONITOR);
// Open saved file and check content
LocalBufferFile bf3 = pdb.openBufferFile();
bufferMgr3 = new BufferMgr(bf3);
assertEquals(bufferCnt,
bufferMgr3.getAllocatedBufferCount() + bufferMgr3.getFreeBufferCount());
for (int i = 1; i < bufferCnt; i += 2) {
try {
bufferMgr3.getBuffer(i);
Assert.fail("Expected deleted buffer: " + i);
}
catch (IOException e) {
// Ignore
}
}
for (int i = 0; i < bufferCnt; i += 10) {
DataBuffer buf = bufferMgr3.getBuffer(i);
assertEquals(buf.getInt(0), i);
bufferMgr3.releaseBuffer(buf);
}
}
finally {
if (bufferMgr != null) {
bufferMgr.dispose();
}
if (bufferMgr2 != null) {
bufferMgr2.dispose();
}
if (bufferMgr3 != null) {
bufferMgr3.dispose();
}
}
}
@Test
public void testRecoveryAfterUndo() throws Exception {
BufferMgr bufferMgr = null;
BufferMgr bufferMgr2 = null;
try {
// Make sure we stress the RecoveryFile storage
int bufferCnt = BUFFER_SIZE * 10;
bufferMgr = init(bufferCnt, BUFFER_SIZE);
bufferCnt += BUFFER_SIZE;
bufferMgr.undo(true);
assertTrue(bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
PrivateDatabase pdb = new PrivateDatabase(testDir);
pdb.refresh();
assertTrue(pdb.canRecover());
// Leave first file open so that recovery files are not removed
// Open a new instance to verify recovery
LocalBufferFile bf2 = pdb.openBufferFileForUpdate();
assertTrue(RecoveryMgr.canRecover(bf2));
bufferMgr2 = new BufferMgr(bf2);
assertTrue(bufferMgr2.recover(TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(bufferMgr2.canSave());
assertEquals(bufferCnt,
bufferMgr2.getAllocatedBufferCount() + bufferMgr2.getFreeBufferCount());
for (int i = 1; i < bufferCnt; i += 2) {
try {
bufferMgr2.getBuffer(i);
Assert.fail("Expected deleted buffer: " + i);
}
catch (IOException e) {
// Ignore
}
}
for (int i = 0; i < bufferCnt; i += 20) {
DataBuffer buf = bufferMgr2.getBuffer(i);
assertEquals(buf.getInt(0), i);
bufferMgr2.releaseBuffer(buf);
}
for (int i = 10; i < bufferCnt; i += 20) {
DataBuffer buf = bufferMgr2.getBuffer(i);
assertEquals(buf.getInt(0), -i);
bufferMgr2.releaseBuffer(buf);
}
}
finally {
if (bufferMgr != null) {
bufferMgr.dispose();
}
if (bufferMgr2 != null) {
bufferMgr2.dispose();
}
}
}
@Test
public void testRecoveryAfterMultiUndo() throws Exception {
BufferMgr bufferMgr = null;
BufferMgr bufferMgr2 = null;
try {
// Make sure we stress the RecoveryFile storage
int bufferCnt = BUFFER_SIZE * 10;
bufferMgr = init(bufferCnt, BUFFER_SIZE);
bufferMgr.undo(true);
bufferMgr.undo(true);
bufferMgr.undo(true);
bufferMgr.undo(true);
assertTrue(bufferMgr.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
PrivateDatabase pdb = new PrivateDatabase(testDir);
pdb.refresh();
assertTrue(pdb.canRecover());
// Leave first file open so that recovery files are not removed
// Open a new instance to verify recovery
LocalBufferFile bf2 = pdb.openBufferFileForUpdate();
assertTrue(RecoveryMgr.canRecover(bf2));
bufferMgr2 = new BufferMgr(bf2);
assertTrue(bufferMgr2.recover(TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(bufferMgr2.canSave());
assertEquals(bufferCnt,
bufferMgr2.getAllocatedBufferCount() + bufferMgr2.getFreeBufferCount());
for (int i = 0; i < bufferCnt; i++) {
DataBuffer buf = bufferMgr2.getBuffer(i);
assertEquals(buf.getInt(0), -i);
bufferMgr2.releaseBuffer(buf);
}
}
finally {
if (bufferMgr != null) {
bufferMgr.dispose();
}
if (bufferMgr2 != null) {
bufferMgr2.dispose();
}
}
}
}

View file

@ -0,0 +1,109 @@
/* ###
* 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.store.local;
import static org.junit.Assert.*;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import ghidra.framework.store.DataFileItem;
public class IndexedLocalFileSystemTest extends AbstractLocalFileSystemTest {
public IndexedLocalFileSystemTest() {
super(true);
}
@Test
public void testIndexRecovery() throws Exception {
testFilePaths();
List<String> names = new ArrayList<>();
for (String itemName : fs.getItemNames("/a/x/bbb")) {
names.add(itemName);
}
// re-instantiate file-system (index will not have been rewritten)
// journal will be replayed to build memory-based index
// IndexedLocalFileSystem.IndexJournal indexJournal =
// (IndexedLocalFileSystem.IndexJournal) getInstanceField("indexJournal", fs);
//indexJournal.close();
fs = LocalFileSystem.getLocalFileSystem(projectDir.getAbsolutePath(), false, false, false,
true);
for (String itemName : names) {
DataFileItem item = (DataFileItem) fs.getItem("/a/x/bbb", itemName);
assertNotNull(item);
assertEquals(itemName, item.getName());
assertEquals("/a/x/bbb", item.getParentPath());
assertEquals("/a/x/bbb/" + itemName, item.getPathName());
InputStream is = item.getInputStream();
assertNotNull(is);
is.close();
}
}
@Test
public void testIndexRebuild() throws Exception {
testFilePaths();
List<String> names = new ArrayList<>();
for (String itemName : fs.getItemNames("/a/x/bbb")) {
names.add(itemName);
}
fs.dispose();
// verify index exists
File indexFile = new File(projectDir, IndexedLocalFileSystem.INDEX_FILE);
assertTrue(indexFile.exists());
File journalFile = new File(projectDir, IndexedLocalFileSystem.JOURNAL_FILE);
assertTrue(!journalFile.exists());
// verify that revised property files can facilitate index rebuild
assertTrue(indexFile.delete());
// can we still identify it as a Indexed FileSystem ?
assertTrue(IndexedLocalFileSystem.hasIndexedStructure(projectDir.getAbsolutePath()));
// reopen filesystem and verify contents after auto-rebuild
fs = LocalFileSystem.getLocalFileSystem(projectDir.getAbsolutePath(), false, false, false,
true);
for (String itemName : names) {
DataFileItem item = (DataFileItem) fs.getItem("/a/x/bbb", itemName);
assertNotNull("/a/x/bbb/" + itemName + " not found", item);
assertEquals(itemName, item.getName());
assertEquals("/a/x/bbb", item.getParentPath());
assertEquals("/a/x/bbb/" + itemName, item.getPathName());
InputStream is = item.getInputStream();
assertNotNull(is);
is.close();
}
}
}

View file

@ -0,0 +1,92 @@
/* ###
* 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.store.local;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.net.URLDecoder;
import java.net.URLEncoder;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.util.NamingUtilities;
import ghidra.util.PropertyFile;
public class IndexedPropertyFileTest extends AbstractGenericTest {
private static String NAME = "IndexTest";
@Test
public void testPropertyFile() throws Exception {
File parent = createTempDirectory(getName());
String storageName = NamingUtilities.mangle(NAME);
PropertyFile pf = new IndexedPropertyFile(parent, storageName, "/", NAME);
assertEquals(storageName, pf.getStorageName());
assertEquals(NAME, pf.getName());
assertEquals("/", pf.getParentPath());
assertEquals("/" + NAME, pf.getPath());
pf.putBoolean("TestBooleanTrue", true);
pf.putBoolean("TestBooleanFalse", false);
pf.putInt("TestInt", 1234);
pf.putLong("TestLong", 0x12345678);
StringBuffer sb = new StringBuffer("Line1\nLine2\n\"Ugly\" & Special <Values>; ");
for (int i = 1; i < 35; i++) {
sb.append((char) i);
}
for (int i = 0x70; i <= 0x80; i++) {
sb.append((char) i);
}
String str = sb.toString();
pf.putString("TestString", URLEncoder.encode(str, "UTF-8"));
pf.writeState();
PropertyFile pf2 = new IndexedPropertyFile(parent, storageName, "/", NAME);
pf2.readState();
assertTrue(pf2.getBoolean("TestBooleanTrue", false));
assertTrue(!pf2.getBoolean("TestBooleanFalse", true));
assertTrue(pf2.getBoolean("TestBooleanBad", true));
assertEquals(1234, pf2.getInt("TestInt", -1));
assertEquals(0x12345678, pf2.getLong("TestLong", -1));
assertEquals(str, URLDecoder.decode(pf2.getString("TestString", null), "UTF-8"));
PropertyFile pf3 =
new IndexedPropertyFile(new File(parent, storageName + PropertyFile.PROPERTY_EXT));
assertEquals(storageName, pf3.getStorageName());
assertEquals(NAME, pf3.getName());
assertEquals("/", pf3.getParentPath());
assertEquals("/" + NAME, pf3.getPath());
assertTrue(pf3.getBoolean("TestBooleanTrue", false));
assertTrue(!pf3.getBoolean("TestBooleanFalse", true));
assertTrue(pf3.getBoolean("TestBooleanBad", true));
assertEquals(1234, pf3.getInt("TestInt", -1));
assertEquals(0x12345678, pf3.getLong("TestLong", -1));
assertEquals(str, URLDecoder.decode(pf3.getString("TestString", null), "UTF-8"));
}
}

View file

@ -0,0 +1,121 @@
/* ###
* 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.store.local;
import static org.junit.Assert.assertTrue;
import java.io.File;
import org.junit.*;
import generic.test.AbstractGenericTest;
import ghidra.util.Msg;
/**
*
*/
public class LockFileTest extends AbstractGenericTest {
private static final File DIRECTORY = new File(AbstractGenericTest.getTestDirectoryPath());
private static final String LOCKNAME = "test";
private static final File LOCKFILE = new File(DIRECTORY, LOCKNAME + ".lock");
private static final int DEFAULT_MAX_LOCK_LEASE_PERIOD_MS = 1500;
private static final int DEFAULT_LOCK_RENEWAL_PERIOD = DEFAULT_MAX_LOCK_LEASE_PERIOD_MS - 200;
/**
* The default timeout for obtaining a lock.
*/
private static final int DEFAULT_TIMEOUT_MS = 3000;
/**
* Constructor for LockFileTest.
* @param arg0
*/
public LockFileTest() {
super();
}
/*
* @see TestCase#setUp()
*/
@Before
public void setUp() throws Exception {
LOCKFILE.delete();
}
/*
* @see TestCase#tearDown()
*/
@After
public void tearDown() throws Exception {
LOCKFILE.delete();
}
@Test
public void testLock() {
LockFile lock1 = new LockFile(DIRECTORY, LOCKNAME, DEFAULT_MAX_LOCK_LEASE_PERIOD_MS,
DEFAULT_LOCK_RENEWAL_PERIOD, DEFAULT_TIMEOUT_MS);
LockFile lock2 = new LockFile(DIRECTORY, LOCKNAME, DEFAULT_MAX_LOCK_LEASE_PERIOD_MS,
DEFAULT_LOCK_RENEWAL_PERIOD, DEFAULT_TIMEOUT_MS);
System.out.println("Create lock: " + lock1);
boolean rc = lock1.createLock(DEFAULT_TIMEOUT_MS, false);
assertTrue(rc);
System.out.println("Create a new lock w/ hold (must wait for old lock to expire)...");
rc = lock2.createLock(DEFAULT_TIMEOUT_MS, true);
assertTrue(rc);
System.out.println("Verify that lock is holding...");
rc = lock1.createLock(DEFAULT_TIMEOUT_MS, false);
assertTrue(!rc);
System.out.println("Verify again that lock is holding...");
rc = lock1.createLock(DEFAULT_TIMEOUT_MS, false);
assertTrue(!rc);
System.out.println("Get second immediate lock...");
rc = lock2.createLock(0, false);
assertTrue(rc);
System.out.println("Remove first lock immediate...");
lock2.removeLock();
assertTrue(lock2.haveLock()); // should still have lock
assertTrue(lock2.haveLock(true));
System.out.println("Remove second lock...");
lock2.removeLock();
assertTrue(!lock2.haveLock()); // should not have lock
assertTrue(!lock2.haveLock(true));
System.out.println("Create immediate lock: " + lock1);
rc = lock1.createLock(0, false);
assertTrue(rc);
Msg.error(this, ">>>>>>>>>>>>>>>> Expected Exception");
lock2.removeLock(); // should have no affect
Msg.error(this, "<<<<<<<<<<<<<<<< End Expected Exception");
assertTrue(lock1.haveLock(true));
System.out.println("Remove lock...");
lock1.removeLock();
assertTrue(!lock1.haveLock(true));
}
}

View file

@ -0,0 +1,97 @@
/* ###
* 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.store.local;
import static org.junit.Assert.*;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import ghidra.framework.store.DataFileItem;
public class MangledLocalFileSystemTest extends AbstractLocalFileSystemTest {
public MangledLocalFileSystemTest() {
super(false);
}
@Test
public void testMigration() throws Exception {
testFilePaths();
List<String> names = new ArrayList<String>();
for (String itemName : fs.getItemNames("/a/x/bbb")) {
names.add(itemName);
}
((MangledLocalFileSystem) fs).convertToIndexedLocalFileSystem();
fs = LocalFileSystem.getLocalFileSystem(projectDir.getAbsolutePath(), false, false, false,
true);
assertEquals(IndexedV1LocalFileSystem.class, fs.getClass());
for (String itemName : names) {
DataFileItem item = (DataFileItem) fs.getItem("/a/x/bbb", itemName);
assertNotNull(item);
assertEquals(itemName, item.getName());
assertEquals("/a/x/bbb", item.getParentPath());
assertEquals("/a/x/bbb/" + itemName, item.getPathName());
InputStream is = item.getInputStream();
assertNotNull(is);
is.close();
}
fs.dispose();
fs = null;
// verify index exists
File indexFile = new File(projectDir, IndexedLocalFileSystem.INDEX_FILE);
assertTrue(indexFile.exists());
File journalFile = new File(projectDir, IndexedLocalFileSystem.JOURNAL_FILE);
assertTrue(!journalFile.exists());
// verify that revised property files can facilitate index rebuild
assertTrue(indexFile.delete());
// can we still identify it as a Indexed FileSystem ?
assertTrue(IndexedLocalFileSystem.hasIndexedStructure(projectDir.getAbsolutePath()));
// reopen filesystem and verify contents after auto-rebuild
fs = LocalFileSystem.getLocalFileSystem(projectDir.getAbsolutePath(), false, false, false,
true);
assertEquals(IndexedV1LocalFileSystem.class, fs.getClass());
for (String itemName : names) {
DataFileItem item = (DataFileItem) fs.getItem("/a/x/bbb", itemName);
assertNotNull(item);
assertEquals(itemName, item.getName());
assertEquals("/a/x/bbb", item.getParentPath());
assertEquals("/a/x/bbb/" + itemName, item.getPathName());
InputStream is = item.getInputStream();
assertNotNull(is);
is.close();
}
}
}

View file

@ -0,0 +1,349 @@
/* ###
* 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 db;
import static org.junit.Assert.*;
import java.io.File;
import org.junit.*;
import db.buffers.BufferFile;
import generic.test.AbstractGenericTest;
import ghidra.framework.store.DatabaseItem;
import ghidra.framework.store.FolderItem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.task.TaskMonitorAdapter;
import utilities.util.FileUtilities;
public class RecoveryDBTest extends AbstractGenericTest {
private static int BUFFER_SIZE = 512;
private static int RECORD_COUNT = 1000;
private static Schema SCHEMA =
new Schema(1, "key", new Class[] { StringField.class }, new String[] { "field1" });
private static final File testDir = new File(AbstractGenericTest.getTestDirectoryPath(), "test");
private LocalFileSystem fileSystem;
/**
* Constructor for RecoveryFileTest.
* @param arg0
*/
public RecoveryDBTest() {
super();
}
@Before
public void setUp() throws Exception {
FileUtilities.deleteDir(testDir);
testDir.mkdir();
fileSystem =
LocalFileSystem.getLocalFileSystem(testDir.getPath(), true, false, false, true);
}
@After
public void tearDown() throws Exception {
fileSystem.dispose();
FileUtilities.deleteDir(testDir);
}
/**
*
* File created:
*
*
* Recovery snapshot taken after each transaction.
* Transaction 1:
*
* Transaction 2:
*
* Transaction 3:
*
* Transaction 4:
*
*
* @throws Exception
*/
private DBHandle init(int initialRecCnt) throws Exception {
DBHandle dbh = new DBHandle(BUFFER_SIZE);
BufferFile bf =
fileSystem.createDatabase("/", "testDb", null, "Test", dbh.getBufferSize(), null, null);
dbh.saveAs(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
dbh.close();
bf.dispose();
DatabaseItem dbItem = (DatabaseItem) fileSystem.getItem("/", "testDb");
assertTrue(!dbItem.canRecover());
bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh = new DBHandle(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
long txId = dbh.startTransaction();
Table table1 = dbh.createTable("table1", SCHEMA);
tableFill(table1, initialRecCnt, "initTable1_");
dbh.endTransaction(txId, true);
txId = dbh.startTransaction();
tableDelete(table1, initialRecCnt, 0, 2);
dbh.endTransaction(txId, true);
assertTrue(dbh.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
txId = dbh.startTransaction();
Table table2 = dbh.createTable("table2", SCHEMA);
tableFill(table2, initialRecCnt, "initTable2_");
dbh.endTransaction(txId, true);
txId = dbh.startTransaction();
tableDelete(table2, initialRecCnt, 0, 2);
dbh.endTransaction(txId, true);
assertTrue(dbh.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
return dbh;
}
private void tableFill(Table table, int recCnt, String baseName) throws Exception {
for (int i = 0; i < recCnt; i++) {
Record rec = SCHEMA.createRecord(i);
rec.setString(0, baseName + i);
table.putRecord(rec);
}
}
private void tableDelete(Table table, int recCnt, int startKey, int inc) throws Exception {
for (int i = startKey; i < recCnt; i += inc) {
table.deleteRecord(i);
}
}
@Test
public void testRecovery() throws Exception {
DBHandle dbh = init(RECORD_COUNT);
DBHandle dbh2 = null;
try {
DatabaseItem dbItem = (DatabaseItem) fileSystem.getItem("/", "testDb");
assertTrue(dbItem.canRecover());
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh2 = new DBHandle(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
Table table1 = dbh2.getTable("table1");
assertNotNull(table1);
assertEquals(RECORD_COUNT / 2, table1.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNull(rec);
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNotNull(rec);
assertEquals("initTable1_" + i, rec.getString(0));
}
Table table2 = dbh2.getTable("table2");
assertNotNull(table2);
assertEquals(RECORD_COUNT / 2, table2.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
assertNull(table2.getRecord(i));
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table2.getRecord(i);
assertNotNull(rec);
assertEquals("initTable2_" + i, rec.getString(0));
}
}
finally {
dbh.close();
if (dbh2 != null) {
dbh2.close();
}
}
}
@Test
public void testRecoveryWithUndo() throws Exception {
DBHandle dbh = init(RECORD_COUNT);
DBHandle dbh2 = null;
try {
assertTrue(dbh.undo());
assertTrue(dbh.undo());
assertTrue(dbh.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
DatabaseItem dbItem = (DatabaseItem) fileSystem.getItem("/", "testDb");
assertTrue(dbItem.canRecover());
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh2 = new DBHandle(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
Table table1 = dbh2.getTable("table1");
assertNotNull(table1);
assertEquals(RECORD_COUNT / 2, table1.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNull(rec);
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNotNull(rec);
assertEquals("initTable1_" + i, rec.getString(0));
}
assertNull(dbh.getTable("table2"));
assertNull(dbh2.getTable("table2"));
}
finally {
dbh.close();
if (dbh2 != null) {
dbh2.close();
}
}
}
@Test
public void testRecoveryWithUndoRedo() throws Exception {
DBHandle dbh = init(RECORD_COUNT);
DBHandle dbh2 = null;
try {
assertTrue(dbh.undo());
assertTrue(dbh.undo());
assertTrue(dbh.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
assertTrue(dbh.redo());
assertTrue(dbh.redo());
assertTrue(dbh.takeRecoverySnapshot(null, TaskMonitorAdapter.DUMMY_MONITOR));
assertNotNull(dbh.getTable("table2"));
DatabaseItem dbItem = (DatabaseItem) fileSystem.getItem("/", "testDb");
assertTrue(dbItem.canRecover());
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh2 = new DBHandle(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
Table table1 = dbh2.getTable("table1");
assertNotNull(table1);
assertEquals(RECORD_COUNT / 2, table1.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNull(rec);
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNotNull(rec);
assertEquals("initTable1_" + i, rec.getString(0));
}
Table table2 = dbh2.getTable("table2");
assertNotNull(table2);
assertEquals(RECORD_COUNT / 2, table2.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
assertNull(table2.getRecord(i));
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table2.getRecord(i);
assertNotNull(rec);
assertEquals("initTable2_" + i, rec.getString(0));
}
}
finally {
dbh.close();
if (dbh2 != null) {
dbh2.close();
}
}
}
@Test
public void testRecoveryWithSave() throws Exception {
DBHandle dbh = init(RECORD_COUNT);
DBHandle dbh2 = null;
try {
DatabaseItem dbItem = (DatabaseItem) fileSystem.getItem("/", "testDb");
assertTrue(dbItem.canRecover());
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh2 = new DBHandle(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
dbh2.save(null, null, TaskMonitorAdapter.DUMMY_MONITOR);
dbh2.close();
assertTrue(!dbItem.canRecover());
bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
dbh2 = new DBHandle(bf);
Table table1 = dbh2.getTable("table1");
assertNotNull(table1);
assertEquals(RECORD_COUNT / 2, table1.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNull(rec);
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table1.getRecord(i);
assertNotNull(rec);
assertEquals("initTable1_" + i, rec.getString(0));
}
Table table2 = dbh2.getTable("table2");
assertNotNull(table2);
assertEquals(RECORD_COUNT / 2, table2.getRecordCount());
for (int i = 0; i < RECORD_COUNT; i += 2) {
assertNull(table2.getRecord(i));
}
for (int i = 1; i < RECORD_COUNT; i += 2) {
Record rec = table2.getRecord(i);
assertNotNull(rec);
assertEquals("initTable2_" + i, rec.getString(0));
}
}
finally {
dbh.close();
if (dbh2 != null) {
dbh2.close();
}
}
}
}

View file

@ -0,0 +1,47 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.store;
import static org.junit.Assert.assertEquals;
import java.util.Date;
import java.util.HashSet;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class FileIDFactoryTest extends AbstractGenericTest {
public FileIDFactoryTest() {
super();
}
@Test
public void testCreateFileID() {
long start = (new Date()).getTime();
HashSet<String> idSet = new HashSet<String>();
int count = 100;
for (int i = 0; i < count; i++) {
idSet.add(FileIDFactory.createFileID());
}
assertEquals(count, idSet.size());
long end = (new Date()).getTime();
long t = (end - start) / count;
System.out.println("FileIDFactoryTest.createFileID average time: " + t + " ms");
}
}

View file

@ -0,0 +1,441 @@
/* ###
* 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.store.db;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import org.junit.*;
import db.*;
import db.buffers.BufferFile;
import db.buffers.LocalBufferFile;
import generic.jar.ResourceFile;
import generic.test.AbstractGenericTest;
import ghidra.util.task.TaskMonitorAdapter;
import utilities.util.FileUtilities;
public class PackedDatabaseTest extends AbstractGenericTest {
private static final Schema TEST_SCHEMA =
new Schema(1, "Key", new Class[] { StringField.class }, new String[] { "Col1" });
private File packedDbFile;
private PackedDatabase db;
private PackedDBHandle dbh;
private PackedDatabase db2;
private PackedDBHandle dbh2;
public PackedDatabaseTest() {
super();
}
@Before
public void setUp() throws Exception {
String path = createTempFilePath("packed.db", ".pf");
packedDbFile = new File(path);
}
@After
public void tearDown() throws Exception {
PackedDatabaseCache cache = PackedDatabaseCache.getCache();
if (cache != null) { // clean-out cacheDir
File cacheDir = (File) getInstanceField("cacheDir", cache);
FileUtilities.deleteDir(cacheDir);
cacheDir.mkdir();
}
if (dbh != null) {
dbh.close();
}
if (db != null) {
ResourceFile pf = db.getPackedFile();
db.dispose();
pf.delete();
}
if (dbh2 != null) {
dbh2.close();
}
if (db2 != null) {
ResourceFile pf = db2.getPackedFile();
db2.dispose();
pf.delete();
}
}
private long createPackedDatabase() throws Exception {
// Create simple database
dbh = new PackedDBHandle("MyContent");
long txId = dbh.startTransaction();
Table table = dbh.createTable("MyTable", TEST_SCHEMA);
Record rec = TEST_SCHEMA.createRecord(1);
rec.setString(0, "String1");
table.putRecord(rec);
dbh.endTransaction(txId, true);
// Create new packed db file
db = dbh.saveAs("Test1", packedDbFile.getParentFile(), packedDbFile.getName(), null);
long id = dbh.getDatabaseId();
dbh.close();
dbh = null;
db.dispose();
return id;
}
@Test
public void testCreatePackedDatabase() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, TaskMonitorAdapter.DUMMY_MONITOR);
assertEquals("MyContent", db.getContentType());
dbh = (PackedDBHandle) db.open(null);
Table table = dbh.getTable("MyTable");
assertNotNull(table);
assertEquals(1, table.getRecordCount());
Record rec = table.getRecord(1);
assertNotNull(rec);
assertEquals("String1", rec.getString(0));
// Second open should fail
try {
dbh2 = (PackedDBHandle) db.openForUpdate(null);
Assert.fail();
}
catch (IOException e) {
// expected failure
}
// Close first one
dbh.close();
db.dispose();
// open for update
db = PackedDatabase.getPackedDatabase(packedDbFile, true, TaskMonitorAdapter.DUMMY_MONITOR);
dbh = (PackedDBHandle) db.openForUpdate(null);
// add record - hold for update
long txId = dbh.startTransaction();
table = dbh.getTable("MyTable");
rec = TEST_SCHEMA.createRecord(2);
rec.setString(0, "String2");
table.putRecord(rec);
dbh.endTransaction(txId, true);
dbh.save(null);
// Test concurrent access by another user
db2 = PackedDatabase.getPackedDatabase(packedDbFile, TaskMonitorAdapter.DUMMY_MONITOR);
assertEquals("MyContent", db2.getContentType());
// Second update access should fail
try {
dbh2 = (PackedDBHandle) db2.openForUpdate(null);
Assert.fail();
}
catch (IOException e) {
// expected lock failure
}
// Read-only access should be allowed
dbh2 = (PackedDBHandle) db2.open(null);
Table table2 = dbh2.getTable("MyTable");
assertNotNull(table2);
assertEquals(2, table2.getRecordCount());
rec = table2.getRecord(1);
assertNotNull(rec);
assertEquals("String1", rec.getString(0));
rec = table2.getRecord(2);
assertNotNull(rec);
assertEquals("String2", rec.getString(0));
}
@Test
public void testCreatePackedDatabaseWithSpecificId() throws Exception {
long id = createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only
db = PackedDatabase.getPackedDatabase(packedDbFile, true, TaskMonitorAdapter.DUMMY_MONITOR);
assertEquals("MyContent", db.getContentType());
dbh = (PackedDBHandle) db.open(null);
assertEquals(id, dbh.getDatabaseId());
// Create new packed db file with different id
File newFile = new File(packedDbFile.getParentFile(), packedDbFile.getName() + "a");
File anotherNewFile = new File(packedDbFile.getParentFile(), packedDbFile.getName() + "b");
try {
long newId = 0x12345678L;
db = dbh.saveAs("Test2", newFile.getParentFile(), newFile.getName(), newId, null);
assertEquals(newId, dbh.getDatabaseId());
db = dbh.saveAs("Test3", anotherNewFile.getParentFile(), anotherNewFile.getName(),
newId, null);
assertEquals(newId, dbh.getDatabaseId());
dbh.close();
dbh = null;
db.dispose();
// Open packed db as read-only
db = PackedDatabase.getPackedDatabase(anotherNewFile, TaskMonitorAdapter.DUMMY_MONITOR);
assertEquals("MyContent", db.getContentType());
dbh = (PackedDBHandle) db.open(null);
assertEquals(newId, dbh.getDatabaseId());
}
finally {
if (dbh != null) {
dbh.close();
dbh = null;
}
newFile.delete();
anotherNewFile.delete();
}
}
@Test
public void testDispose() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, true, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
db.dispose();
assertTrue(!dbDir.exists());
assertTrue(!tmpDbDir.exists());
}
@Test
public void testDispose_Cached() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
db.dispose();
assertTrue(dbDir.exists());
assertTrue(!tmpDbDir.exists());
}
@Test
public void testAutoDisposeOnClose() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, true, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
dbh = (PackedDBHandle) db.open(TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
dbh.close();
assertTrue(!dbDir.exists());
assertTrue(!tmpDbDir.exists());
}
@Test
public void testCache() throws Exception {
createPackedDatabase();
File commaFile = new File(packedDbFile.getParentFile(), "a,b,c,d.pdb");
commaFile.delete();
assertFalse(commaFile.exists());
packedDbFile.renameTo(commaFile);
assertTrue(commaFile.exists());
PackedDatabaseCache cache = PackedDatabaseCache.getCache();
assertTrue(PackedDatabaseCache.isEnabled());
ResourceFile dbFile = new ResourceFile(commaFile);
cache.purgeFromCache(dbFile);
assertFalse(cache.isInCache(dbFile));
try {
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(commaFile, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
dbh = (PackedDBHandle) db.open(TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(dbDir.isDirectory());
assertTrue(cache.isInCache(dbFile));
dbh.close();
db.dispose();
assertTrue(dbDir.exists());
assertTrue(cache.isInCache(dbFile));
PackedDatabase cachedDB = cache.getCachedDB(dbFile, TaskMonitorAdapter.DUMMY_MONITOR);
assertNotNull(cachedDB);
cachedDB.dispose();
assertTrue(dbDir.exists());
assertTrue(cache.isInCache(dbFile));
// reopen
db = PackedDatabase.getPackedDatabase(commaFile, TaskMonitorAdapter.DUMMY_MONITOR);
dbh = (PackedDBHandle) db.open(TaskMonitorAdapter.DUMMY_MONITOR);
assertEquals(dbDir, getInstanceField("dbDir", db));
assertTrue(cache.isInCache(dbFile));
dbh.close();
db.dispose();
}
finally {
cache.purgeFromCache(dbFile);
}
}
@Test
public void testAutoDisposeOnClose_Cached() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
PackedDatabaseCache cache = PackedDatabaseCache.getCache();
assertTrue(PackedDatabaseCache.isEnabled());
ResourceFile dbFile = new ResourceFile(packedDbFile);
cache.purgeFromCache(dbFile);
assertFalse(cache.isInCache(dbFile));
try {
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
dbh = (PackedDBHandle) db.open(TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
assertTrue(cache.isInCache(dbFile));
dbh.close();
assertTrue(dbDir.exists());
assertTrue(!tmpDbDir.exists());
assertTrue(cache.isInCache(dbFile));
}
finally {
cache.purgeFromCache(dbFile);
}
}
@Test
public void testAutoDisposeOnSaveAs() throws Exception {
createPackedDatabase();
assertTrue(packedDbFile.exists());
// Open packed db as read-only and verify content
db = PackedDatabase.getPackedDatabase(packedDbFile, true, TaskMonitorAdapter.DUMMY_MONITOR);
File dbDir = (File) getInstanceField("dbDir", db);
File tmpDbDir = new File(dbDir.getParentFile(), dbDir.getName() + ".delete");
File tmpFile1 = createTempFile(getName() + "1", ".gbf");
tmpFile1.delete();
File tmpFile2 = createTempFile(getName() + "2", ".gbf");
tmpFile2.delete();
BufferFile bf = null;
dbh = (PackedDBHandle) db.open(TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
bf = new LocalBufferFile(tmpFile1, dbh.getBufferSize());
dbh.saveAs(bf, false, TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(bf.isReadOnly());
bf.dispose();
// original dbh still refers to unpacked database
assertTrue(dbDir.isDirectory());
assertTrue(!tmpDbDir.exists());
assertTrue(tmpFile1.exists()); // still in-use
bf = new LocalBufferFile(tmpFile2, dbh.getBufferSize());
dbh.saveAs(bf, true, TaskMonitorAdapter.DUMMY_MONITOR);
assertTrue(bf.isReadOnly());
assertTrue(tmpFile1.exists()); // no longer in-use
assertTrue(tmpFile1.delete());
// original dbh now refers to new database - original packed database should close
assertTrue(!dbDir.exists());
assertTrue(!tmpDbDir.exists());
assertTrue(tmpFile2.exists()); // no longer in-use
dbh.close(); // must close before removing
assertTrue(tmpFile2.delete());
}
}

View file

@ -0,0 +1,144 @@
/* ###
* 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.store.db;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import org.junit.*;
import db.buffers.*;
import generic.test.AbstractGenericTest;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.framework.store.local.LocalFolderItem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import mockit.*;
import utilities.util.FileUtilities;
public class VersionFailureRecoveryTest extends AbstractGenericTest {
private File testDir = new File(getTestDirectoryPath(), "VersionFailureRecoveryTest");
private File testFile = new File(getTestDirectoryPath(), "TestBufferFile.tmp");
private LocalFileSystem versionedFileSystem;
@Before
public void setUp() throws Exception {
testFile.delete();
FileUtilities.deleteDir(testDir);
testDir.mkdir();
versionedFileSystem =
LocalFileSystem.getLocalFileSystem(testDir.getAbsolutePath(), true, true, false, false);
}
@After
public void tearDown() throws Exception {
testFile.delete();
versionedFileSystem.dispose();
FileUtilities.deleteDir(testDir);
}
/**
* This test is intended to verify that an IOException thrown during the streaming creation
* of a new versioned file will properly cleanup and not leave an invalid database item
*/
@Test
public void testAddToVersionControlFailure() {
new FakeBadBufferFile(); // setup for mocking
LocalBufferFile fakeBadBufferFile = null;
try {
fakeBadBufferFile = new LocalBufferFile(testFile, BufferMgr.DEFAULT_BUFFER_SIZE);
versionedFileSystem.createDatabase("/", "test", "xFILEIDx", fakeBadBufferFile,
"comment", "PROGRAM", false, TaskMonitor.DUMMY, "test-user");
fail("Expected IOException");
}
catch (InvalidNameException e) {
fail("unexpected");
}
catch (CancelledException e) {
fail("unexpected");
}
catch (IOException e) {
assertEquals("forced block read failure", e.getMessage());
}
finally {
if (fakeBadBufferFile != null) {
fakeBadBufferFile.delete();
}
}
try {
LocalFolderItem item = versionedFileSystem.getItem("/", "test");
assertNull("Did not expect item to exist in filesystem", item);
}
catch (IOException e) {
failWithException("Unexpected IOException", e);
}
}
private class FakeBadBufferFile extends MockUp<LocalBufferFile> {
@Mock
public int getIndexCount() {
return 10;
}
@Mock
public InputBlockStream getInputBlockStream(Invocation invocation) {
LocalBufferFile bufferFile = invocation.getInvokedInstance();
return new InputBlockStream() {
@Override
public boolean includesHeaderBlock() {
return true;
}
@Override
public void close() throws IOException {
// ignore
}
@Override
public int getBlockSize() {
return bufferFile.getBufferSize();
}
@Override
public BufferFileBlock readBlock() throws IOException {
throw new IOException("forced block read failure");
}
@Override
public int getBlockCount() {
return bufferFile.getIndexCount();
}
};
}
}
}