mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
GP-4085 Added ability to add VTSession to a shared repository
This commit is contained in:
parent
b8f004c792
commit
c3386b72a2
33 changed files with 1063 additions and 565 deletions
|
@ -424,7 +424,6 @@ public class HeadlessAnalyzer {
|
||||||
|
|
||||||
if (locator.getProjectDir().exists()) {
|
if (locator.getProjectDir().exists()) {
|
||||||
project = openProject(locator);
|
project = openProject(locator);
|
||||||
AppInfo.setActiveProject(project);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (options.runScriptsNoImport) {
|
if (options.runScriptsNoImport) {
|
||||||
|
@ -441,7 +440,6 @@ public class HeadlessAnalyzer {
|
||||||
Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") +
|
Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") +
|
||||||
"project: " + locator);
|
"project: " + locator);
|
||||||
project = getProjectManager().createProject(locator, null, false);
|
project = getProjectManager().createProject(locator, null, false);
|
||||||
AppInfo.setActiveProject(project);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -459,7 +457,6 @@ public class HeadlessAnalyzer {
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
project.close();
|
project.close();
|
||||||
AppInfo.setActiveProject(null);
|
|
||||||
if (!options.runScriptsNoImport && options.deleteProject) {
|
if (!options.runScriptsNoImport && options.deleteProject) {
|
||||||
FileUtilities.deleteDir(locator.getProjectDir());
|
FileUtilities.deleteDir(locator.getProjectDir());
|
||||||
locator.getMarkerFile().delete();
|
locator.getMarkerFile().delete();
|
||||||
|
@ -1841,11 +1838,13 @@ public class HeadlessAnalyzer {
|
||||||
HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection)
|
HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
super(projectManager, connection);
|
super(projectManager, connection);
|
||||||
|
AppInfo.setActiveProject(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator)
|
HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator)
|
||||||
throws NotOwnerException, LockException, IOException {
|
throws NotOwnerException, LockException, IOException {
|
||||||
super(projectManager, projectLocator, false);
|
super(projectManager, projectLocator, false);
|
||||||
|
AppInfo.setActiveProject(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ import ghidra.util.task.TaskMonitor;
|
||||||
public class ProgramOpener {
|
public class ProgramOpener {
|
||||||
private final Object consumer;
|
private final Object consumer;
|
||||||
private String openPromptText = "Open";
|
private String openPromptText = "Open";
|
||||||
private boolean silent = false; // if true operation does not permit interaction
|
private boolean silent = SystemUtilities.isInHeadlessMode(); // if true operation does not permit interaction
|
||||||
private boolean noCheckout = false; // if true operation should not perform optional checkout
|
private boolean noCheckout = false; // if true operation should not perform optional checkout
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -253,8 +253,9 @@ public class ProgramOpener {
|
||||||
if (domainFile.checkout(dialog.exclusiveCheckout(), monitor)) {
|
if (domainFile.checkout(dialog.exclusiveCheckout(), monitor)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Msg.showError(this, null, "Checkout Failed", "Exclusive checkout failed for: " +
|
Msg.showError(this, null, "Checkout Failed",
|
||||||
domainFile.getName() + "\nOne or more users have file checked out!");
|
"Exclusive checkout failed for: " + domainFile.getName() +
|
||||||
|
"\nOne or more users have file checked out!");
|
||||||
}
|
}
|
||||||
catch (CancelledException e) {
|
catch (CancelledException e) {
|
||||||
// we don't care, the task has been cancelled
|
// we don't care, the task has been cancelled
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// A script that runs Auto Version Tracking given the options set in one of the following ways:
|
// A script that runs Auto Version Tracking given the options set in one of the following ways:
|
||||||
// 1. If script is run from the CodeBrowser, the GUI options are set in a pop up dialog by user.
|
// 1. If script is run from the CodeBrowser, the GUI options are set in a pop up dialog by user.
|
||||||
// 2. If script is run in headless mode either the defaults provided by the script are used or the
|
// 2. If script is run in headless mode either the defaults provided by the script are used or the
|
||||||
|
@ -129,6 +130,8 @@ public class AutoVersionTrackingScript extends GhidraScript {
|
||||||
Program otherProgram = startupValues.getProgram("Please select the other program", this,
|
Program otherProgram = startupValues.getProgram("Please select the other program", this,
|
||||||
state.getTool(), autoUpgradeIfNeeded);
|
state.getTool(), autoUpgradeIfNeeded);
|
||||||
|
|
||||||
|
VTSession session = null;
|
||||||
|
try {
|
||||||
if (isCurrentProgramSourceProg) {
|
if (isCurrentProgramSourceProg) {
|
||||||
sourceProgram = currentProgram;
|
sourceProgram = currentProgram;
|
||||||
destinationProgram = otherProgram;
|
destinationProgram = otherProgram;
|
||||||
|
@ -145,8 +148,7 @@ public class AutoVersionTrackingScript extends GhidraScript {
|
||||||
// Need to end the script transaction or it interferes with vt things that need locks
|
// Need to end the script transaction or it interferes with vt things that need locks
|
||||||
end(true);
|
end(true);
|
||||||
|
|
||||||
VTSession session =
|
session = new VTSessionDB(name, sourceProgram, destinationProgram, this);
|
||||||
VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this);
|
|
||||||
|
|
||||||
if (folder.getFile(name) == null) {
|
if (folder.getFile(name) == null) {
|
||||||
folder.createFile(name, session, monitor);
|
folder.createFile(name, session, monitor);
|
||||||
|
@ -180,17 +182,21 @@ public class AutoVersionTrackingScript extends GhidraScript {
|
||||||
|
|
||||||
TaskLauncher.launch(autoVtTask);
|
TaskLauncher.launch(autoVtTask);
|
||||||
|
|
||||||
// if not running headless user can decide whether to save or not
|
// Save destination program and session changes
|
||||||
// if running headless - must save here or nothing that was done in this script will be
|
|
||||||
// accessible later.
|
|
||||||
if (isRunningHeadless()) {
|
|
||||||
otherProgram.save("Updated with Auto Version Tracking", monitor);
|
otherProgram.save("Updated with Auto Version Tracking", monitor);
|
||||||
session.save();
|
session.save();
|
||||||
}
|
|
||||||
|
|
||||||
println(autoVtTask.getStatusMsg());
|
println(autoVtTask.getStatusMsg());
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (otherProgram != null) {
|
||||||
otherProgram.release(this);
|
otherProgram.release(this);
|
||||||
}
|
}
|
||||||
|
if (session != null) {
|
||||||
|
session.release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to determine if there is an existing VTSession in the given folder with the given name
|
* Method to determine if there is an existing VTSession in the given folder with the given name
|
||||||
|
|
|
@ -63,8 +63,7 @@ public class CreateAppliedExactMatchingSessionScript extends GhidraScript {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
VTSession session =
|
VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this);
|
||||||
VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this);
|
|
||||||
|
|
||||||
// it seems clunky to have to create this separately, but I'm not sure how else to do it
|
// it seems clunky to have to create this separately, but I'm not sure how else to do it
|
||||||
folder.createFile(name, session, monitor);
|
folder.createFile(name, session, monitor);
|
||||||
|
|
|
@ -20,14 +20,14 @@
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import ghidra.feature.vt.GhidraVersionTrackingScript;
|
import ghidra.feature.vt.AbstractGhidraVersionTrackingScript;
|
||||||
import ghidra.feature.vt.api.main.VTMatch;
|
import ghidra.feature.vt.api.main.VTMatch;
|
||||||
import ghidra.framework.model.Project;
|
import ghidra.framework.model.Project;
|
||||||
import ghidra.program.model.listing.Function;
|
import ghidra.program.model.listing.Function;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public class FindChangedFunctionsScript extends GhidraVersionTrackingScript {
|
public class FindChangedFunctionsScript extends AbstractGhidraVersionTrackingScript {
|
||||||
|
|
||||||
private Program p1;
|
private Program p1;
|
||||||
private Program p2;
|
private Program p2;
|
||||||
|
|
|
@ -20,10 +20,10 @@
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import ghidra.feature.vt.GhidraVersionTrackingScript;
|
import ghidra.feature.vt.AbstractGhidraVersionTrackingScript;
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
|
|
||||||
public class OpenVersionTrackingSessionScript extends GhidraVersionTrackingScript {
|
public class OpenVersionTrackingSessionScript extends AbstractGhidraVersionTrackingScript {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void run() throws Exception {
|
protected void run() throws Exception {
|
||||||
|
@ -33,6 +33,9 @@ public class OpenVersionTrackingSessionScript extends GhidraVersionTrackingScrip
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acceptMatchesWithGoodConfidence() throws Exception {
|
private void acceptMatchesWithGoodConfidence() throws Exception {
|
||||||
|
|
||||||
|
VTSession vtSession = getVTSession();
|
||||||
|
|
||||||
println("Working on session: " + vtSession);
|
println("Working on session: " + vtSession);
|
||||||
|
|
||||||
List<VTMatchSet> matchSets = vtSession.getMatchSets();
|
List<VTMatchSet> matchSets = vtSession.getMatchSets();
|
||||||
|
|
|
@ -25,50 +25,81 @@ import ghidra.feature.vt.api.util.VTOptions;
|
||||||
import ghidra.framework.model.DomainFile;
|
import ghidra.framework.model.DomainFile;
|
||||||
import ghidra.framework.model.DomainFolder;
|
import ghidra.framework.model.DomainFolder;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.util.InvalidNameException;
|
||||||
import ghidra.util.classfinder.ClassSearcher;
|
import ghidra.util.classfinder.ClassSearcher;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.exception.VersionException;
|
import ghidra.util.exception.VersionException;
|
||||||
|
|
||||||
public abstract class GhidraVersionTrackingScript extends GhidraScript {
|
public abstract class AbstractGhidraVersionTrackingScript extends GhidraScript {
|
||||||
protected VTSession vtSession;
|
private VTSession vtSession;
|
||||||
protected Program sourceProgram;
|
private Program sourceProgram;
|
||||||
protected Program destinationProgram;
|
private Program destinationProgram;
|
||||||
|
|
||||||
private int transactionID;
|
private int transactionID;
|
||||||
|
|
||||||
public void createVersionTrackingSession(String sourceProgramPath,
|
protected VTSession getVTSession() {
|
||||||
String destinationProgramPath) throws Exception {
|
return vtSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Program getSourceProgram() {
|
||||||
|
return sourceProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Program getDestinationProgram() {
|
||||||
|
return destinationProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VTSession createVersionTrackingSession(String sourceProgramPath,
|
||||||
|
String destinationProgramPath)
|
||||||
|
throws VersionException, CancelledException, IOException {
|
||||||
|
|
||||||
if (vtSession != null) {
|
if (vtSession != null) {
|
||||||
throw new RuntimeException("Attempted to open a new session with one already open!");
|
throw new RuntimeException("Attempted to open a new session with one already open!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
sourceProgram = openProgram(sourceProgramPath);
|
sourceProgram = openProgram(sourceProgramPath);
|
||||||
|
|
||||||
destinationProgram = openProgram(destinationProgramPath);
|
destinationProgram = openProgram(destinationProgramPath);
|
||||||
|
|
||||||
createVersionTrackingSession("New Session", sourceProgram, destinationProgram);
|
vtSession = new VTSessionDB("New Session", sourceProgram, destinationProgram, this);
|
||||||
|
transactionID = vtSession.startTransaction("VT Script");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (vtSession == null) {
|
||||||
|
closeVersionTrackingSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vtSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createVersionTrackingSession(String name, Program source, Program destination)
|
public VTSession createVersionTrackingSession(String name, Program source, Program destination)
|
||||||
throws Exception {
|
throws IOException {
|
||||||
|
|
||||||
if (vtSession != null) {
|
if (vtSession != null) {
|
||||||
throw new RuntimeException("Attempted to create a new session with one already open!");
|
throw new RuntimeException("Attempted to create a new session with one already open!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
sourceProgram = source;
|
sourceProgram = source;
|
||||||
destinationProgram = destination;
|
|
||||||
|
|
||||||
if (!sourceProgram.isUsedBy(this)) {
|
|
||||||
sourceProgram.addConsumer(this);
|
sourceProgram.addConsumer(this);
|
||||||
}
|
|
||||||
if (!destinationProgram.isUsedBy(this)) {
|
|
||||||
destinationProgram.addConsumer(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
vtSession = VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this);
|
destinationProgram = destination;
|
||||||
|
destinationProgram.addConsumer(this);
|
||||||
|
|
||||||
|
vtSession = new VTSessionDB(name, sourceProgram, destinationProgram, this);
|
||||||
transactionID = vtSession.startTransaction("VT Script");
|
transactionID = vtSession.startTransaction("VT Script");
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
if (vtSession == null) {
|
||||||
|
closeVersionTrackingSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vtSession;
|
||||||
|
}
|
||||||
|
|
||||||
public void openVersionTrackingSession(String path) throws Exception {
|
public VTSession openVersionTrackingSession(String path)
|
||||||
|
throws VersionException, CancelledException, IOException {
|
||||||
if (vtSession != null) {
|
if (vtSession != null) {
|
||||||
throw new RuntimeException("Attempted to open a session with one already open!");
|
throw new RuntimeException("Attempted to open a session with one already open!");
|
||||||
}
|
}
|
||||||
|
@ -79,51 +110,72 @@ public abstract class GhidraVersionTrackingScript extends GhidraScript {
|
||||||
DomainFile file = state.getProject().getProjectData().getFile(path);
|
DomainFile file = state.getProject().getProjectData().getFile(path);
|
||||||
vtSession = (VTSessionDB) file.getDomainObject(this, true, true, monitor);
|
vtSession = (VTSessionDB) file.getDomainObject(this, true, true, monitor);
|
||||||
sourceProgram = vtSession.getSourceProgram();
|
sourceProgram = vtSession.getSourceProgram();
|
||||||
destinationProgram = vtSession.getDestinationProgram();
|
|
||||||
|
|
||||||
if (!sourceProgram.isUsedBy(this)) {
|
|
||||||
sourceProgram.addConsumer(this);
|
sourceProgram.addConsumer(this);
|
||||||
}
|
destinationProgram = vtSession.getDestinationProgram();
|
||||||
if (!destinationProgram.isUsedBy(this)) {
|
|
||||||
destinationProgram.addConsumer(this);
|
destinationProgram.addConsumer(this);
|
||||||
}
|
|
||||||
transactionID = vtSession.startTransaction("VT Script");
|
transactionID = vtSession.startTransaction("VT Script");
|
||||||
|
return vtSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveVersionTrackingSession() throws IOException {
|
public void saveVersionTrackingSession() throws IOException {
|
||||||
|
if (vtSession != null) {
|
||||||
|
throw new RuntimeException("Attempted to save a session when not open!");
|
||||||
|
}
|
||||||
vtSession.endTransaction(transactionID, true);
|
vtSession.endTransaction(transactionID, true);
|
||||||
|
try {
|
||||||
vtSession.save();
|
vtSession.save();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
transactionID = vtSession.startTransaction("VT Script");
|
transactionID = vtSession.startTransaction("VT Script");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void saveSessionAs(String path, String name) throws Exception {
|
public void saveSessionAs(String path, String name)
|
||||||
|
throws InvalidNameException, CancelledException, IOException {
|
||||||
|
if (vtSession != null) {
|
||||||
|
throw new RuntimeException("Attempted to save a session when not open!");
|
||||||
|
}
|
||||||
|
vtSession.endTransaction(transactionID, true);
|
||||||
|
try {
|
||||||
DomainFolder folder = state.getProject().getProjectData().getFolder(path);
|
DomainFolder folder = state.getProject().getProjectData().getFolder(path);
|
||||||
folder.createFile(name, vtSession, monitor);
|
folder.createFile(name, vtSession, monitor);
|
||||||
vtSession.setName(name);
|
vtSession.setName(name);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
transactionID = vtSession.startTransaction("VT Script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup(boolean success) {
|
public void cleanup(boolean success) {
|
||||||
closeVersionTrackingSession();
|
closeVersionTrackingSession();
|
||||||
if (destinationProgram != null) {
|
super.cleanup(success);
|
||||||
closeProgram(destinationProgram);
|
|
||||||
}
|
|
||||||
if (sourceProgram != null) {
|
|
||||||
closeProgram(sourceProgram);
|
|
||||||
}
|
|
||||||
sourceProgram = null;
|
|
||||||
destinationProgram = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will release the current session and both source and destination programs.
|
||||||
|
* If either program needs to be held it is the script's responsibility to first retain
|
||||||
|
* the instance and add itself as a consumer. Any program consumer must release it
|
||||||
|
* when done using it.
|
||||||
|
*/
|
||||||
public void closeVersionTrackingSession() {
|
public void closeVersionTrackingSession() {
|
||||||
if (vtSession != null) {
|
if (vtSession != null) {
|
||||||
vtSession.endTransaction(transactionID, true);
|
vtSession.endTransaction(transactionID, true);
|
||||||
vtSession.release(this);
|
vtSession.release(this);
|
||||||
|
vtSession = null;
|
||||||
|
}
|
||||||
|
if (destinationProgram != null) {
|
||||||
|
destinationProgram.release(this);
|
||||||
|
destinationProgram = null;
|
||||||
|
}
|
||||||
|
if (sourceProgram != null) {
|
||||||
|
sourceProgram.release(this);
|
||||||
|
sourceProgram = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private Program openProgram(String path)
|
||||||
|
|
||||||
public Program openProgram(String path)
|
|
||||||
throws VersionException, CancelledException, IOException {
|
throws VersionException, CancelledException, IOException {
|
||||||
if (state.getProject() == null) {
|
if (state.getProject() == null) {
|
||||||
throw new RuntimeException("No project open.");
|
throw new RuntimeException("No project open.");
|
||||||
|
@ -132,11 +184,6 @@ public abstract class GhidraVersionTrackingScript extends GhidraScript {
|
||||||
return (Program) file.getDomainObject(this, true, true, monitor);
|
return (Program) file.getDomainObject(this, true, true, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void closeProgram(Program program) {
|
|
||||||
program.release(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getSourceFunctions() {
|
public Set<String> getSourceFunctions() {
|
||||||
if (vtSession == null) {
|
if (vtSession == null) {
|
||||||
throw new RuntimeException("You must have an open vt session");
|
throw new RuntimeException("You must have an open vt session");
|
|
@ -13,33 +13,30 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.api.impl;
|
package ghidra.feature.vt.api.db;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
|
|
||||||
import db.DBHandle;
|
import db.DBHandle;
|
||||||
import db.OpenMode;
|
|
||||||
import db.buffers.BufferFile;
|
import db.buffers.BufferFile;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.feature.vt.api.db.VTSessionDB;
|
|
||||||
import ghidra.framework.data.DBContentHandler;
|
import ghidra.framework.data.DBContentHandler;
|
||||||
import ghidra.framework.data.DomainObjectMergeManager;
|
import ghidra.framework.data.DomainObjectMergeManager;
|
||||||
import ghidra.framework.model.ChangeSet;
|
import ghidra.framework.model.ChangeSet;
|
||||||
import ghidra.framework.model.DomainObject;
|
import ghidra.framework.model.DomainObject;
|
||||||
import ghidra.framework.store.*;
|
import ghidra.framework.store.*;
|
||||||
import ghidra.util.InvalidNameException;
|
import ghidra.util.*;
|
||||||
import ghidra.util.Msg;
|
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.exception.VersionException;
|
import ghidra.util.exception.VersionException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||||
|
|
||||||
private static Icon ICON = new GIcon("icon.version.tracking.session.content.type");
|
public static final String CONTENT_TYPE = "VersionTracking";
|
||||||
|
|
||||||
public final static String CONTENT_TYPE = "VersionTracking";
|
private static final Icon ICON = new GIcon("icon.version.tracking.session.content.type");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
|
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
|
||||||
|
@ -74,22 +71,40 @@ public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||||
return "Version Tracking";
|
return "Version Tracking";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkContentAndExclusiveCheckout(FolderItem item) throws IOException {
|
||||||
|
String contentType = item.getContentType();
|
||||||
|
if (!contentType.equals(CONTENT_TYPE)) {
|
||||||
|
throw new IOException("Unsupported content type: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: item.isVersioned indicates that item is located on versioned filesystem
|
||||||
|
// and is not checked-out, otheriwse assume item in local filesystem and must
|
||||||
|
// ensure if any checkout is exclusive.
|
||||||
|
if (item.isVersioned() || (item.isCheckedOut() && !item.isCheckedOutExclusive())) {
|
||||||
|
throw new IOException(
|
||||||
|
"Unsupported VT Session use: session file must be checked-out exclusive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||||
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
|
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException, VersionException {
|
throws IOException, CancelledException, VersionException {
|
||||||
|
|
||||||
String contentType = item.getContentType();
|
checkContentAndExclusiveCheckout(item);
|
||||||
if (!contentType.equals(CONTENT_TYPE)) {
|
|
||||||
throw new IOException("Unsupported content type: " + contentType);
|
if (item.isReadOnly()) {
|
||||||
|
throw new ReadOnlyException("VT Session file is set read-only which prevents its use");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DatabaseItem dbItem = (DatabaseItem) item;
|
DatabaseItem dbItem = (DatabaseItem) item;
|
||||||
BufferFile bf = dbItem.openForUpdate(checkoutId);
|
BufferFile bf = dbItem.openForUpdate(checkoutId);
|
||||||
DBHandle dbh = new DBHandle(bf, okToRecover, monitor);
|
DBHandle dbh = new DBHandle(bf, okToRecover, monitor);
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
VTSessionDB db = VTSessionDB.getVTSession(dbh, OpenMode.UPGRADE, consumer, monitor);
|
// NOTE: Always open with DB upgrade enabled
|
||||||
|
VTSessionDB db = new VTSessionDB(dbh, monitor, consumer);
|
||||||
success = true;
|
success = true;
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
@ -99,13 +114,7 @@ public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (VersionException e) {
|
catch (VersionException | IOException | CancelledException e) {
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
catch (CancelledException e) {
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
catch (Throwable t) {
|
catch (Throwable t) {
|
||||||
|
@ -134,12 +143,7 @@ public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||||
int minChangeVersion, TaskMonitor monitor)
|
int minChangeVersion, TaskMonitor monitor)
|
||||||
throws IOException, CancelledException, VersionException {
|
throws IOException, CancelledException, VersionException {
|
||||||
|
|
||||||
String contentType = item.getContentType();
|
|
||||||
if (!contentType.equals(CONTENT_TYPE)) {
|
|
||||||
throw new IOException("Unsupported content type: " + contentType);
|
|
||||||
}
|
|
||||||
return getReadOnlyObject(item, -1, false, consumer, monitor);
|
return getReadOnlyObject(item, -1, false, consumer, monitor);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -154,43 +158,14 @@ public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||||
Object consumer, TaskMonitor monitor)
|
Object consumer, TaskMonitor monitor)
|
||||||
throws IOException, VersionException, CancelledException {
|
throws IOException, VersionException, CancelledException {
|
||||||
|
|
||||||
String contentType = item.getContentType();
|
checkContentAndExclusiveCheckout(item);
|
||||||
if (contentType != null && !contentType.equals(CONTENT_TYPE)) {
|
|
||||||
throw new IOException("Unsupported content type: " + contentType);
|
throw new ReadOnlyException("VT Session does not support read-only use");
|
||||||
}
|
|
||||||
try {
|
|
||||||
DatabaseItem dbItem = (DatabaseItem) item;
|
|
||||||
BufferFile bf = dbItem.open();
|
|
||||||
DBHandle dbh = new DBHandle(bf);
|
|
||||||
boolean success = false;
|
|
||||||
try {
|
|
||||||
VTSessionDB manager =
|
|
||||||
VTSessionDB.getVTSession(dbh, OpenMode.READ_ONLY, consumer, monitor);
|
|
||||||
success = true;
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (!success) {
|
|
||||||
dbh.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
catch (Throwable t) {
|
|
||||||
Msg.error(this, "Get read-only object failed", t);
|
|
||||||
String msg = t.getMessage();
|
|
||||||
if (msg == null) {
|
|
||||||
msg = t.toString();
|
|
||||||
}
|
|
||||||
throw new IOException("Open failed: " + msg, t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPrivateContentType() {
|
public boolean isPrivateContentType() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -26,6 +26,7 @@ import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
|
||||||
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
||||||
import ghidra.feature.vt.api.impl.*;
|
import ghidra.feature.vt.api.impl.*;
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
|
import ghidra.feature.vt.api.util.VTSessionFileUtil;
|
||||||
import ghidra.framework.data.DomainObjectAdapterDB;
|
import ghidra.framework.data.DomainObjectAdapterDB;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.model.TransactionInfo.Status;
|
import ghidra.framework.model.TransactionInfo.Status;
|
||||||
|
@ -36,26 +37,30 @@ import ghidra.program.database.map.AddressMap;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
import ghidra.program.model.address.AddressSet;
|
import ghidra.program.model.address.AddressSet;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.*;
|
||||||
import ghidra.util.exception.*;
|
import ghidra.util.exception.*;
|
||||||
import ghidra.util.task.TaskLauncher;
|
import ghidra.util.task.TaskLauncher;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
|
|
||||||
private final static Field[] COL_FIELDS = new Field[] { StringField.INSTANCE };
|
private final static Field[] COL_FIELDS = new Field[] { StringField.INSTANCE };
|
||||||
private final static String[] COL_TYPES = new String[] { "Value" };
|
private final static String[] COL_TYPES = new String[] { "Value" };
|
||||||
private final static Schema SCHEMA =
|
private final static Schema SCHEMA =
|
||||||
new Schema(0, StringField.INSTANCE, "Key", COL_FIELDS, COL_TYPES);
|
new Schema(0, StringField.INSTANCE, "Key", COL_FIELDS, COL_TYPES);
|
||||||
|
|
||||||
private static final String PROGRAM_ID_PROPERTYLIST_NAME = "ProgramIDs";
|
// Source and Destination Program IDs are retained within OptionsDB
|
||||||
private static final String SOURCE_PROGRAM_ID_PROPERTY_KEY = "SourceProgramID";
|
static final String PROGRAM_ID_PROPERTYLIST_NAME = "ProgramIDs";
|
||||||
private static final String DESTINATION_PROGRAM_ID_PROPERTY_KEY = "DestinationProgramID";
|
static final String SOURCE_PROGRAM_ID_PROPERTY_KEY = "SourceProgramID";
|
||||||
|
static final String DESTINATION_PROGRAM_ID_PROPERTY_KEY = "DestinationProgramID";
|
||||||
|
|
||||||
private static final String UNUSED_DEFAULT_NAME = "Untitled";
|
private static final String UNUSED_DEFAULT_NAME = "Untitled";
|
||||||
private static final int EVENT_NOTIFICATION_DELAY = 500;
|
private static final int EVENT_NOTIFICATION_DELAY = 500;
|
||||||
private static final int EVENT_BUFFER_SIZE = 100;
|
|
||||||
|
|
||||||
private static final long MANUAL_MATCH_SET_ID = 0;
|
private static final long MANUAL_MATCH_SET_ID = 0;
|
||||||
private static final long IMPLIED_MATCH_SET_ID = -1;
|
private static final long IMPLIED_MATCH_SET_ID = -1;
|
||||||
|
|
||||||
|
// PropertyTable is used solely to retain DB version
|
||||||
|
// NOTE: OptionsDB already has a table named "Property Table"
|
||||||
private static final String PROPERTY_TABLE_NAME = "PropertyTable";
|
private static final String PROPERTY_TABLE_NAME = "PropertyTable";
|
||||||
private static final String DB_VERSION_PROPERTY_NAME = "DB_VERSION";
|
private static final String DB_VERSION_PROPERTY_NAME = "DB_VERSION";
|
||||||
|
|
||||||
|
@ -65,8 +70,11 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
* 14-Nov-2019 - version 2 - Corrected fixed length indexing implementation causing
|
* 14-Nov-2019 - version 2 - Corrected fixed length indexing implementation causing
|
||||||
* change in index table low-level storage for newly
|
* change in index table low-level storage for newly
|
||||||
* created tables.
|
* created tables.
|
||||||
|
* 16-Feb-2024 - version 3 - No schema change. Version imposed to prevent older versions
|
||||||
|
* of Ghidra from opening session objects which may have been
|
||||||
|
* added to version controlled repository.
|
||||||
*/
|
*/
|
||||||
private static final int DB_VERSION = 2;
|
private static final int DB_VERSION = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPGRADE_REQUIRED_BFORE_VERSION should be changed to DB_VERSION any time the
|
* UPGRADE_REQUIRED_BFORE_VERSION should be changed to DB_VERSION any time the
|
||||||
|
@ -75,7 +83,7 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
* if the data's version is >= UPGRADE_REQUIRED_BEFORE_VERSION and <= DB_VERSION.
|
* if the data's version is >= UPGRADE_REQUIRED_BEFORE_VERSION and <= DB_VERSION.
|
||||||
*/
|
*/
|
||||||
// NOTE: Schema upgrades are not currently supported
|
// NOTE: Schema upgrades are not currently supported
|
||||||
private static final int UPGRADE_REQUIRED_BEFORE_VERSION = 1;
|
private static final int UPGRADE_REQUIRED_BEFORE_VERSION = 3;
|
||||||
|
|
||||||
private VTMatchSetTableDBAdapter matchSetTableAdapter;
|
private VTMatchSetTableDBAdapter matchSetTableAdapter;
|
||||||
private AssociationDatabaseManager associationManager;
|
private AssociationDatabaseManager associationManager;
|
||||||
|
@ -89,40 +97,119 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
private VTMatchSet impliedMatchSet;
|
private VTMatchSet impliedMatchSet;
|
||||||
|
|
||||||
private boolean changeSetsModified = false;
|
private boolean changeSetsModified = false;
|
||||||
private Table propertyTable;
|
private Table propertyTable; // used to retain DB version only
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method which constructs a new VTSessionDB using specified source and desitination
|
||||||
|
* programs.
|
||||||
|
* @param name name to be assigned to the resulting domain object file
|
||||||
|
* @param sourceProgram session source program within active project
|
||||||
|
* @param destinationProgram session destination program open for update within active project
|
||||||
|
* @param consumer object consumer resposible for the proper release of the returned instance.
|
||||||
|
* @return new {@link VTSessionDB} object
|
||||||
|
* @throws IOException if an IO error occurs
|
||||||
|
* @deprecated {@link #VTSessionDB(String, Program, Program, Object)} should be used instead
|
||||||
|
*/
|
||||||
|
@Deprecated(since = "11.1", forRemoval = true)
|
||||||
public static VTSessionDB createVTSession(String name, Program sourceProgram,
|
public static VTSessionDB createVTSession(String name, Program sourceProgram,
|
||||||
Program destinationProgram, Object consumer) throws IOException {
|
Program destinationProgram, Object consumer) throws IOException {
|
||||||
|
return new VTSessionDB(name, sourceProgram, destinationProgram, consumer);
|
||||||
|
}
|
||||||
|
|
||||||
VTSessionDB session = new VTSessionDB(new DBHandle(), consumer);
|
/**
|
||||||
|
* Construct a new VTSessionDB using specified source and desitination programs.
|
||||||
|
* @param name name to be assigned to the resulting domain object file
|
||||||
|
* @param sourceProgram session source program within active project
|
||||||
|
* @param destinationProgram session destination program open for update within active project
|
||||||
|
* @param consumer object consumer resposible for the proper release of the returned instance.
|
||||||
|
* @throws IOException if an IO error occurs
|
||||||
|
*/
|
||||||
|
public VTSessionDB(String name, Program sourceProgram, Program destinationProgram,
|
||||||
|
Object consumer) throws IOException {
|
||||||
|
super(new DBHandle(), UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer);
|
||||||
|
|
||||||
int ID = session.startTransaction("Constructing New Version Tracking Match Set");
|
propertyTable = dbh.getTable(PROPERTY_TABLE_NAME);
|
||||||
|
|
||||||
|
int ID = startTransaction("Constructing New Version Tracking Match Set");
|
||||||
try {
|
try {
|
||||||
session.propertyTable = session.dbh.createTable(PROPERTY_TABLE_NAME, SCHEMA);
|
propertyTable = dbh.createTable(PROPERTY_TABLE_NAME, SCHEMA);
|
||||||
session.matchSetTableAdapter = VTMatchSetTableDBAdapter.createAdapter(session.dbh);
|
matchSetTableAdapter = VTMatchSetTableDBAdapter.createAdapter(dbh);
|
||||||
session.associationManager =
|
associationManager = AssociationDatabaseManager.createAssociationManager(dbh, this);
|
||||||
AssociationDatabaseManager.createAssociationManager(session.dbh, session);
|
matchTagAdapter = VTMatchTagDBAdapter.createAdapter(dbh);
|
||||||
session.matchTagAdapter = VTMatchTagDBAdapter.createAdapter(session.dbh);
|
|
||||||
session.initializePrograms(sourceProgram, destinationProgram);
|
initializePrograms(sourceProgram, destinationProgram, true);
|
||||||
session.createMatchSet(
|
|
||||||
new ManualMatchProgramCorrelator(sourceProgram, destinationProgram),
|
createMatchSet(new ManualMatchProgramCorrelator(sourceProgram, destinationProgram),
|
||||||
MANUAL_MATCH_SET_ID);
|
MANUAL_MATCH_SET_ID);
|
||||||
session.createMatchSet(
|
createMatchSet(new ImpliedMatchProgramCorrelator(sourceProgram, destinationProgram),
|
||||||
new ImpliedMatchProgramCorrelator(sourceProgram, destinationProgram),
|
|
||||||
IMPLIED_MATCH_SET_ID);
|
IMPLIED_MATCH_SET_ID);
|
||||||
session.updateVersion();
|
|
||||||
|
updateVersion();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
session.endTransaction(ID, true);
|
endTransaction(ID, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session.addSynchronizedDomainObject(destinationProgram);
|
addSynchronizedDomainObject(destinationProgram);
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
session.close();
|
close();
|
||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return session;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an existing VT session object and open with UPGRADE enabled.
|
||||||
|
* The caller (i.e., content handler) must ensure that project has exclusive access to
|
||||||
|
* the domain file before it was open and {@link DBHandle} supplied.
|
||||||
|
* @param dbHandle database handle
|
||||||
|
* @param monitor TaskMonitor that allows the open to be canceled.
|
||||||
|
* @param consumer the object that keeping the session open.
|
||||||
|
* @throws IOException if an error accessing the database occurs.
|
||||||
|
* @throws VersionException if database version does not match implementation, UPGRADE may be possible.
|
||||||
|
* @throws CancelledException if instantiation is canceled by monitor
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
VTSessionDB(DBHandle dbHandle, TaskMonitor monitor, Object consumer)
|
||||||
|
throws VersionException, IOException, CancelledException {
|
||||||
|
super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer);
|
||||||
|
|
||||||
|
// openMode forced to UPGRADE since we do not support read-only mode
|
||||||
|
// It is assumed we always have exclusive access to the underlying database
|
||||||
|
OpenMode openMode = OpenMode.UPGRADE;
|
||||||
|
|
||||||
|
propertyTable = dbHandle.getTable(PROPERTY_TABLE_NAME);
|
||||||
|
|
||||||
|
int storedVersion = getVersion();
|
||||||
|
if (storedVersion > DB_VERSION) {
|
||||||
|
throw new VersionException(VersionException.NEWER_VERSION, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following version logic holds true for DB_VERSION <= 3 which assume no additional
|
||||||
|
// DB index tables will be added when open for update/upgrade. This may not hold
|
||||||
|
// true for future revisions associated with table schema changes in which case the
|
||||||
|
// UPGRADE_REQUIRED_BEFORE_VERSION value should equal DB_VERSION. Current logic
|
||||||
|
// assumes no schema changes will be made during upgrade.
|
||||||
|
if (storedVersion < UPGRADE_REQUIRED_BEFORE_VERSION) {
|
||||||
|
if (openMode != OpenMode.UPGRADE) { // should always be open with UPGRADE mode
|
||||||
|
throw new VersionException(
|
||||||
|
"Version Tracking Sessions do not support schema upgrades.",
|
||||||
|
VersionException.OLDER_VERSION, true);
|
||||||
|
}
|
||||||
|
withTransaction("Update DBVersion", () -> updateVersion());
|
||||||
|
clearUndo(false);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: code below will not make changes (no transaction is open)
|
||||||
|
// Additional supported required to facilitate schema change during upgrade if needed.
|
||||||
|
|
||||||
|
matchSetTableAdapter = VTMatchSetTableDBAdapter.getAdapter(dbHandle, openMode, monitor);
|
||||||
|
associationManager =
|
||||||
|
AssociationDatabaseManager.getAssociationManager(dbHandle, this, openMode, monitor);
|
||||||
|
matchTagAdapter = VTMatchTagDBAdapter.getAdapter(dbHandle, openMode, monitor);
|
||||||
|
loadMatchSets(openMode, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVersion() throws IOException {
|
private void updateVersion() throws IOException {
|
||||||
|
@ -131,47 +218,174 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
propertyTable.putRecord(record);
|
propertyTable.putRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VTSessionDB getVTSession(DBHandle dbHandle, OpenMode openMode, Object consumer,
|
private int getVersion() throws IOException {
|
||||||
TaskMonitor monitor) throws VersionException, IOException {
|
// DB Version was added in release (11/6/2012)
|
||||||
|
// if record does not exist return 0;
|
||||||
VTSessionDB session = new VTSessionDB(dbHandle, consumer);
|
if (propertyTable == null) {
|
||||||
int storedVersion = session.getVersion();
|
return 0;
|
||||||
|
|
||||||
if (storedVersion > DB_VERSION) {
|
|
||||||
throw new VersionException(VersionException.NEWER_VERSION, false);
|
|
||||||
}
|
}
|
||||||
// The following version logic holds true for DB_VERSION=2 which assumes no additional
|
DBRecord record = propertyTable.getRecord(new StringField(DB_VERSION_PROPERTY_NAME));
|
||||||
// DB index tables will be added when open for update/upgrade. This will not hold
|
|
||||||
// true for future revisions associated with table schema changes in which case the
|
if (record != null) {
|
||||||
// UPGRADE_REQUIRED_BEFORE_VERSION value should equal DB_VERSION.
|
String s = record.getString(0);
|
||||||
if (storedVersion < UPGRADE_REQUIRED_BEFORE_VERSION) {
|
try {
|
||||||
throw new VersionException("Version Tracking Sessions do not support schema upgrades.");
|
return Integer.parseInt(s);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e) {
|
||||||
|
// just use default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.matchSetTableAdapter =
|
@Override
|
||||||
VTMatchSetTableDBAdapter.getAdapter(session.getDBHandle(), openMode, monitor);
|
protected void setDomainFile(DomainFile df) throws DomainObjectException {
|
||||||
session.associationManager =
|
DomainFolder parent = df.getParent();
|
||||||
AssociationDatabaseManager.getAssociationManager(dbHandle, session, openMode, monitor);
|
if (parent != null && sourceProgram == null) {
|
||||||
session.matchTagAdapter =
|
try {
|
||||||
VTMatchTagDBAdapter.getAdapter(session.getDBHandle(), openMode, monitor);
|
openSourceAndDestinationPrograms(parent.getProjectData());
|
||||||
session.loadMatchSets(openMode, monitor);
|
}
|
||||||
return session;
|
catch (IOException e) {
|
||||||
|
throw new DomainObjectException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.setDomainFile(df);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open associated source and destination program files and complete session initialization.
|
||||||
|
* @param projectData active project data
|
||||||
|
* @throws IOException if source or destination program not found within specified project
|
||||||
|
* or an error occured while opening them (e.g., upgrade required).
|
||||||
|
*/
|
||||||
|
private void openSourceAndDestinationPrograms(ProjectData projectData) throws IOException {
|
||||||
|
String sourceProgramID = getSourceProgramID();
|
||||||
|
String destinationProgramID = getDestinationProgramID();
|
||||||
|
DomainFile sourceFile = projectData.getFileByID(sourceProgramID);
|
||||||
|
DomainFile destinationFile = projectData.getFileByID(destinationProgramID);
|
||||||
|
if (sourceFile == null) {
|
||||||
|
throw new IOException("Source program is missing for this Version Tracking Session!");
|
||||||
|
}
|
||||||
|
if (destinationFile == null) {
|
||||||
|
throw new IOException(
|
||||||
|
"Destination program is missing for this Version Tracking Session!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must ensure that destination program file can be updated
|
||||||
|
VTSessionFileUtil.validateDestinationProgramFile(destinationFile, true,
|
||||||
|
SystemUtilities.isInHeadlessMode());
|
||||||
|
|
||||||
|
VTSessionFileUtil.validateSourceProgramFile(sourceFile, true);
|
||||||
|
|
||||||
|
sourceProgram = openProgram(sourceFile, true);
|
||||||
|
|
||||||
|
if (sourceProgram != null) {
|
||||||
|
destinationProgram = openProgram(destinationFile, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceProgram == null || destinationProgram == null) {
|
||||||
|
StringBuilder buffer = new StringBuilder(
|
||||||
|
"Session not opened because one or both programs did not open.\n");
|
||||||
|
if (sourceProgram != null) {
|
||||||
|
sourceProgram.release(this);
|
||||||
|
sourceProgram = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.append("\tUnable to open source program \"" + sourceFile + "\"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationProgram != null) {
|
||||||
|
destinationProgram.release(this);
|
||||||
|
destinationProgram = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.append("\tUnable to open destination program \"" + destinationFile + "\"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(buffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
associationManager.sessionInitialized();
|
||||||
|
|
||||||
|
try {
|
||||||
|
addSynchronizedDomainObject(destinationProgram);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
sourceProgram.release(this);
|
||||||
|
sourceProgram = null;
|
||||||
|
destinationProgram.release(this);
|
||||||
|
destinationProgram = null;
|
||||||
|
throw new IOException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Program openProgram(DomainFile domainFile, boolean isSource) {
|
||||||
|
|
||||||
|
String type = isSource ? "VT Source Program" : "VT Destination Program";
|
||||||
|
|
||||||
|
if (SystemUtilities.isInHeadlessMode()) {
|
||||||
|
try {
|
||||||
|
return (Program) domainFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
throw new AssertionError(e); // unexpected
|
||||||
|
}
|
||||||
|
catch (VersionException e) {
|
||||||
|
VersionExceptionHandler.showVersionError(null, domainFile.getName(), type, "open",
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
Msg.showError(this, null, "Can't open " + type + ": " + domainFile.getName(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headed GUI Mode
|
||||||
|
|
||||||
|
OpenProgramTask openTask = new OpenProgramTask(domainFile, this);
|
||||||
|
openTask.setOpenPromptText("Open " + type);
|
||||||
|
|
||||||
|
TaskLauncher.launch(openTask);
|
||||||
|
|
||||||
|
OpenProgramRequest openProgram = openTask.getOpenProgram();
|
||||||
|
return openProgram != null ? openProgram.getProgram() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceProgramID() {
|
||||||
|
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
||||||
|
return properties.getString(SOURCE_PROGRAM_ID_PROPERTY_KEY, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestinationProgramID() {
|
||||||
|
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
||||||
|
return properties.getString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("hiding")
|
@SuppressWarnings("hiding")
|
||||||
// this is from our constructor
|
// this is from our constructor
|
||||||
private void initializePrograms(Program sourceProgram, Program destinationProgram) {
|
private void initializePrograms(Program sourceProgram, Program destinationProgram,
|
||||||
|
boolean rememberProgramIds) throws IOException {
|
||||||
|
|
||||||
|
if (!destinationProgram.canSave()) {
|
||||||
|
throw new ReadOnlyException(
|
||||||
|
"VT Session destination program is read-only which prevents its use");
|
||||||
|
}
|
||||||
|
|
||||||
this.sourceProgram = sourceProgram;
|
this.sourceProgram = sourceProgram;
|
||||||
this.destinationProgram = destinationProgram;
|
|
||||||
sourceProgram.addConsumer(this);
|
sourceProgram.addConsumer(this);
|
||||||
|
|
||||||
|
this.destinationProgram = destinationProgram;
|
||||||
destinationProgram.addConsumer(this);
|
destinationProgram.addConsumer(this);
|
||||||
|
|
||||||
|
if (rememberProgramIds) {
|
||||||
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
||||||
DomainFile sourceDomainFile = sourceProgram.getDomainFile();
|
DomainFile sourceDomainFile = sourceProgram.getDomainFile();
|
||||||
properties.setString(SOURCE_PROGRAM_ID_PROPERTY_KEY, sourceDomainFile.getFileID());
|
properties.setString(SOURCE_PROGRAM_ID_PROPERTY_KEY, sourceDomainFile.getFileID());
|
||||||
DomainFile destinationDomainFile = destinationProgram.getDomainFile();
|
DomainFile destinationDomainFile = destinationProgram.getDomainFile();
|
||||||
properties.setString(DESTINATION_PROGRAM_ID_PROPERTY_KEY,
|
properties.setString(DESTINATION_PROGRAM_ID_PROPERTY_KEY,
|
||||||
destinationDomainFile.getFileID());
|
destinationDomainFile.getFileID());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -202,119 +416,6 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
|
||||||
sourceProgram.addConsumer(this);
|
sourceProgram.addConsumer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSourceProgramID() {
|
|
||||||
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
|
||||||
return properties.getString(SOURCE_PROGRAM_ID_PROPERTY_KEY, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDestinationProgramID() {
|
|
||||||
Options properties = getOptions(PROGRAM_ID_PROPERTYLIST_NAME);
|
|
||||||
return properties.getString(DESTINATION_PROGRAM_ID_PROPERTY_KEY, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private VTSessionDB(DBHandle dbHandle, Object consumer) {
|
|
||||||
super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer);
|
|
||||||
propertyTable = dbHandle.getTable(PROPERTY_TABLE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getVersion() throws IOException {
|
|
||||||
// DB Version was added in release (11/6/2012)
|
|
||||||
// if record does not exist return 0;
|
|
||||||
if (propertyTable == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
DBRecord record = propertyTable.getRecord(new StringField(DB_VERSION_PROPERTY_NAME));
|
|
||||||
|
|
||||||
if (record != null) {
|
|
||||||
String s = record.getString(0);
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(s);
|
|
||||||
}
|
|
||||||
catch (NumberFormatException e) {
|
|
||||||
// just use default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setDomainFile(DomainFile df) {
|
|
||||||
super.setDomainFile(df);
|
|
||||||
DomainFolder parent = df.getParent();
|
|
||||||
if (parent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sourceProgram != null) { // source and destination are already open
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ProjectData projectData = parent.getProjectData();
|
|
||||||
String sourceProgramID = getSourceProgramID();
|
|
||||||
String destinationProgramID = getDestinationProgramID();
|
|
||||||
DomainFile sourceFile = projectData.getFileByID(sourceProgramID);
|
|
||||||
DomainFile destinationFile = projectData.getFileByID(destinationProgramID);
|
|
||||||
if (sourceFile == null) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Source program is missing for this Version Tracking Session!");
|
|
||||||
}
|
|
||||||
if (destinationFile == null) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Destination program is missing for this Version Tracking Session!");
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceProgram = openProgram(sourceFile, true);
|
|
||||||
|
|
||||||
if (sourceProgram != null) {
|
|
||||||
destinationProgram = openProgram(destinationFile, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceProgram == null || destinationProgram == null) {
|
|
||||||
StringBuilder buffer = new StringBuilder(
|
|
||||||
"Session not opened because one or both programs did not open.\n");
|
|
||||||
if (sourceProgram != null) {
|
|
||||||
sourceProgram.release(this);
|
|
||||||
sourceProgram = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buffer.append("\tUnable to open source program \"" + sourceFile + "\"\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destinationProgram != null) {
|
|
||||||
destinationProgram.release(this);
|
|
||||||
destinationProgram = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buffer.append("\tUnable to open destination program \"" + destinationFile + "\"\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException(buffer.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
associationManager.sessionInitialized();
|
|
||||||
|
|
||||||
try {
|
|
||||||
addSynchronizedDomainObject(destinationProgram);
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
sourceProgram.release(this);
|
|
||||||
destinationProgram.release(this);
|
|
||||||
throw new RuntimeException(e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Program openProgram(DomainFile domainFile, boolean isSource) {
|
|
||||||
|
|
||||||
OpenProgramTask openTask = new OpenProgramTask(domainFile, this);
|
|
||||||
String type = isSource ? "(source program)" : "(destination program)";
|
|
||||||
openTask.setOpenPromptText("Open " + type);
|
|
||||||
|
|
||||||
TaskLauncher.launch(openTask);
|
|
||||||
|
|
||||||
OpenProgramRequest openProgram = openTask.getOpenProgram();
|
|
||||||
return openProgram != null ? openProgram.getProgram() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release(Object consumer) {
|
public void release(Object consumer) {
|
||||||
super.release(consumer);
|
super.release(consumer);
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
/* ###
|
||||||
|
* 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.feature.vt.api.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import ghidra.app.util.dialog.CheckoutDialog;
|
||||||
|
import ghidra.app.util.task.ProgramOpener;
|
||||||
|
import ghidra.feature.vt.api.db.VTSessionDB;
|
||||||
|
import ghidra.framework.model.DomainFile;
|
||||||
|
import ghidra.framework.model.DomainFolder;
|
||||||
|
import ghidra.framework.remote.User;
|
||||||
|
import ghidra.program.database.ProgramDB;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.SystemUtilities;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link VTSessionFileUtil} provides methods for checking {@link VTSessionDB} source and
|
||||||
|
* destination program files prior to being opened and used during session instantiation.
|
||||||
|
*/
|
||||||
|
public class VTSessionFileUtil {
|
||||||
|
|
||||||
|
// static utility class
|
||||||
|
private VTSessionFileUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a VT source program to ensure it meets minimum criteria to open with a VTSession.
|
||||||
|
* The following validation checks are performed:
|
||||||
|
* <ul>
|
||||||
|
* <li>file must correspond to a ProgramDB</li>
|
||||||
|
* </ul>
|
||||||
|
* If an error is thrown it is intended to be augmented for proper presentation.
|
||||||
|
*
|
||||||
|
* @param file VT Session source program domain file
|
||||||
|
* @param includeFilePathInError if true file path will be appended to any exception throw
|
||||||
|
* @throws IllegalArgumentException if any VT source program file criteria is not satisfied
|
||||||
|
*/
|
||||||
|
public static void validateSourceProgramFile(DomainFile file, boolean includeFilePathInError)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
String error = null;
|
||||||
|
if (!ProgramDB.class.isAssignableFrom(file.getDomainObjectClass())) {
|
||||||
|
error = "Source file does not correspond to a Program";
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
if (includeFilePathInError) {
|
||||||
|
error += ":\n" + file.getPathname();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a VT destination program to ensure it meets minimum criteria to open with a VTSession.
|
||||||
|
* GUI mode only: If file is versioned and not checked-out the user may be prompted to perform
|
||||||
|
* an optional checkout of the file. Prompting for checkout will not occur if this method
|
||||||
|
* is invoked from the Swing thread or operating in a headless mode.
|
||||||
|
* The following validation checks are performed:
|
||||||
|
* <ul>
|
||||||
|
* <li>file must correspond to a ProgramDB</li>
|
||||||
|
* <li>file must be contained within the active project</li>
|
||||||
|
* <li>file must not be marked read-only</li>
|
||||||
|
* <li>if file is versioned it must be checked-out (user may be prompted to do this)</li>
|
||||||
|
* </ul>
|
||||||
|
* If an error is thrown it is intended to be augmented for proper presentation.
|
||||||
|
*
|
||||||
|
* @param file VT Session destination program domain file
|
||||||
|
* @param includeFilePathInError if true file path will be appended to any exception throw
|
||||||
|
* @param silent if user interaction should not be performed. This should be true if
|
||||||
|
* filesystem lock is currently held.
|
||||||
|
* @throws IllegalArgumentException if any VT destination program file criteria is not satisfied
|
||||||
|
*/
|
||||||
|
public static void validateDestinationProgramFile(DomainFile file,
|
||||||
|
boolean includeFilePathInError, boolean silent) throws IllegalArgumentException {
|
||||||
|
String error = null;
|
||||||
|
if (!ProgramDB.class.isAssignableFrom(file.getDomainObjectClass())) {
|
||||||
|
error = "Destination file does not correspond to a Program";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DomainFolder folder = file.getParent();
|
||||||
|
if (folder == null || !folder.isInWritableProject()) {
|
||||||
|
error = "Destination file must be from active project";
|
||||||
|
}
|
||||||
|
else if (file.isReadOnly()) {
|
||||||
|
error = "Destination file must not be read-only";
|
||||||
|
}
|
||||||
|
else if (file.isVersioned()) {
|
||||||
|
if (!silent) {
|
||||||
|
doOptionalDestinationProgramCheckout(file);
|
||||||
|
}
|
||||||
|
if (!file.isCheckedOut()) {
|
||||||
|
error = "Versioned destination file must be checked-out for update";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
if (includeFilePathInError) {
|
||||||
|
error += ":\n" + file.getPathname();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the specified {@link DomainFile} will permit update.
|
||||||
|
* @param file domain file
|
||||||
|
* @return true if file permits update else false
|
||||||
|
*/
|
||||||
|
public static boolean canUpdate(DomainFile file) {
|
||||||
|
DomainFolder folder = file.getParent();
|
||||||
|
if (folder == null || !folder.isInWritableProject()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.isReadOnly()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.isVersioned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void doOptionalDestinationProgramCheckout(DomainFile file) {
|
||||||
|
|
||||||
|
if (SystemUtilities.isInHeadlessMode() || !file.canCheckout()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = file.getParent().getProjectData().getUser();
|
||||||
|
CheckoutDialog dialog = new CheckoutDialog(file, user);
|
||||||
|
dialog.setTitle("VT Destination Program not Checked Out");
|
||||||
|
if (dialog.showDialog() == CheckoutDialog.CHECKOUT) { // uses Swing thread
|
||||||
|
CheckoutDestinationProgramTask task =
|
||||||
|
new CheckoutDestinationProgramTask(file, dialog.exclusiveCheckout());
|
||||||
|
TaskLauncher.launch(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CheckoutDestinationProgramTask extends Task {
|
||||||
|
|
||||||
|
private DomainFile file;
|
||||||
|
boolean exclusiveCheckout;
|
||||||
|
|
||||||
|
CheckoutDestinationProgramTask(DomainFile file, boolean exclusiveCheckout) {
|
||||||
|
super("Checking Out " + file, true, true, true, true);
|
||||||
|
this.file = file;
|
||||||
|
this.exclusiveCheckout = exclusiveCheckout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
monitor.setMessage("Checking Out " + file);
|
||||||
|
try {
|
||||||
|
if (!file.checkout(exclusiveCheckout, monitor)) {
|
||||||
|
Msg.showError(ProgramOpener.class, null, "Checkout Failed",
|
||||||
|
"Exclusive checkout failed for: " + file +
|
||||||
|
"\nOne or more users have file checked out!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
Msg.showError(ProgramOpener.class, null, "Checkout Failed",
|
||||||
|
"Checkout failed for: " + file + "\n" + e.getMessage());
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -46,7 +46,7 @@ public interface VTController extends VTSessionSupplier {
|
||||||
@Override
|
@Override
|
||||||
public VTSession getSession();
|
public VTSession getSession();
|
||||||
|
|
||||||
public void openVersionTrackingSession(DomainFile domainFile);
|
public boolean openVersionTrackingSession(DomainFile domainFile);
|
||||||
|
|
||||||
public void openVersionTrackingSession(VTSession session);
|
public void openVersionTrackingSession(VTSession session);
|
||||||
|
|
||||||
|
|
|
@ -22,31 +22,36 @@ import java.util.*;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
|
|
||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
|
import docking.widgets.OptionDialog;
|
||||||
import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext;
|
import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext;
|
||||||
import ghidra.app.plugin.core.colorizer.ColorizingService;
|
import ghidra.app.plugin.core.colorizer.ColorizingService;
|
||||||
import ghidra.feature.vt.api.db.VTAssociationDB;
|
import ghidra.feature.vt.api.db.VTAssociationDB;
|
||||||
import ghidra.feature.vt.api.db.VTSessionDB;
|
import ghidra.feature.vt.api.db.VTSessionDB;
|
||||||
import ghidra.feature.vt.api.main.*;
|
import ghidra.feature.vt.api.main.*;
|
||||||
|
import ghidra.feature.vt.api.util.VTSessionFileUtil;
|
||||||
import ghidra.feature.vt.gui.duallisting.VTListingContext;
|
import ghidra.feature.vt.gui.duallisting.VTListingContext;
|
||||||
import ghidra.feature.vt.gui.provider.markuptable.VTMarkupItemContext;
|
import ghidra.feature.vt.gui.provider.markuptable.VTMarkupItemContext;
|
||||||
import ghidra.feature.vt.gui.task.SaveTask;
|
import ghidra.feature.vt.gui.task.SaveTask;
|
||||||
import ghidra.feature.vt.gui.task.VtTask;
|
import ghidra.feature.vt.gui.task.VtTask;
|
||||||
import ghidra.feature.vt.gui.util.MatchInfo;
|
import ghidra.feature.vt.gui.util.MatchInfo;
|
||||||
import ghidra.feature.vt.gui.util.MatchInfoFactory;
|
import ghidra.feature.vt.gui.util.MatchInfoFactory;
|
||||||
|
import ghidra.framework.client.RepositoryAdapter;
|
||||||
import ghidra.framework.data.DomainObjectAdapterDB;
|
import ghidra.framework.data.DomainObjectAdapterDB;
|
||||||
|
import ghidra.framework.main.AppInfo;
|
||||||
import ghidra.framework.main.SaveDataDialog;
|
import ghidra.framework.main.SaveDataDialog;
|
||||||
|
import ghidra.framework.main.projectdata.actions.CheckoutsDialog;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.options.*;
|
import ghidra.framework.options.*;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.framework.plugintool.ServiceProvider;
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.framework.store.ItemCheckoutStatus;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
import ghidra.program.model.address.AddressSetView;
|
import ghidra.program.model.address.AddressSetView;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
import ghidra.program.model.symbol.Symbol;
|
import ghidra.program.model.symbol.Symbol;
|
||||||
import ghidra.program.util.AddressCorrelation;
|
import ghidra.program.util.AddressCorrelation;
|
||||||
import ghidra.program.util.ProgramLocation;
|
import ghidra.program.util.ProgramLocation;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.*;
|
||||||
import ghidra.util.SystemUtilities;
|
|
||||||
import ghidra.util.datastruct.WeakValueHashMap;
|
import ghidra.util.datastruct.WeakValueHashMap;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.exception.VersionException;
|
import ghidra.util.exception.VersionException;
|
||||||
|
@ -94,30 +99,193 @@ public class VTControllerImpl
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean checkSessionFileAccess(DomainFile domainFile) {
|
||||||
public void openVersionTrackingSession(DomainFile domainFile) {
|
|
||||||
if (!checkForUnSavedChanges()) {
|
DomainFolder folder = domainFile.getParent();
|
||||||
return;
|
if (folder == null || !folder.isInWritableProject()) {
|
||||||
|
Msg.showError(this, null, "Can't open VT Session: " + domainFile,
|
||||||
|
"VT Session file use limited to active project only.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
if (domainFile.isVersioned()) {
|
||||||
|
if (domainFile.isCheckedOut()) {
|
||||||
|
if (!domainFile.isCheckedOutExclusive()) {
|
||||||
|
Msg.showError(this, null, "Can't open VT Session: " + domainFile,
|
||||||
|
"VT Session file is checked-out but does not have exclusive access.\n" +
|
||||||
|
"You must undo checkout and re-checkout with exclusive access.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (domainFile.isReadOnly()) {
|
||||||
|
Msg.showError(this, null, "Can't open VT Session: " + domainFile,
|
||||||
|
"VT Session file is set read-only which prevents its use.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return checkoutSession(domainFile);
|
||||||
|
}
|
||||||
|
else if (domainFile.isReadOnly()) { // non-versioned file
|
||||||
|
Msg.showError(this, null, "Can't open VT Session: " + domainFile,
|
||||||
|
"VT Session file is set read-only which prevents its use.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkoutSession(DomainFile domainFile) {
|
||||||
|
|
||||||
|
Project activeProject = AppInfo.getActiveProject();
|
||||||
|
RepositoryAdapter repository = activeProject.getRepository();
|
||||||
|
|
||||||
|
if (repository != null) {
|
||||||
try {
|
try {
|
||||||
VTSessionDB newSession =
|
ItemCheckoutStatus[] checkouts = domainFile.getCheckouts();
|
||||||
(VTSessionDB) domainFile.getDomainObject(this, true, true, TaskMonitor.DUMMY);
|
if (checkouts.length != 0) {
|
||||||
doOpenSession(newSession);
|
int rc = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
|
||||||
|
"Checkout VT Session",
|
||||||
|
"VT Session " + domainFile.getName() + " is NOT CHECKED OUT but " +
|
||||||
|
"is checked-out by another user.\n" +
|
||||||
|
"Opening VT Session requires an exclusive check out of this file.\n" +
|
||||||
|
"Do you want to view the list of active checkouts for this file?",
|
||||||
|
"View Checkout(s)...");
|
||||||
|
if (rc != OptionDialog.OPTION_ONE) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (VersionException e) {
|
|
||||||
Msg.showError(this, null, "Can't open domainFile " + domainFile.getName(),
|
CheckoutsDialog dialog = new CheckoutsDialog(plugin.getTool(),
|
||||||
e.getMessage());
|
repository.getUser(), domainFile, checkouts);
|
||||||
|
plugin.getTool().showDialog(dialog);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (CancelledException e) {
|
|
||||||
Msg.error(this, "Got unexexped cancelled exception", e);
|
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
Msg.showError(this, null, "Can't open " + domainFile.getName(), e.getMessage());
|
Msg.showError(this, null, "Checkout VT Session Failed: " + domainFile.getName(),
|
||||||
|
e.getMessage());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int rc = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, "Checkout VT Session",
|
||||||
|
"VT Session " + domainFile.getName() + " is NOT CHECKED OUT.\n" +
|
||||||
|
"Opening VT Session requires an exclusive check out of this file.\n" +
|
||||||
|
"Do you want to Check Out this file?",
|
||||||
|
"Checkout...");
|
||||||
|
if (rc != OptionDialog.OPTION_ONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskLauncher.launchModal("Checkout VT Session", new MonitoredRunnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void monitoredRun(TaskMonitor monitor) {
|
||||||
|
try {
|
||||||
|
domainFile.checkout(true, monitor);
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
Msg.showError(this, null, "Checkout VT Session Failed: " + domainFile.getName(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return domainFile.isCheckedOutExclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean openVersionTrackingSession(DomainFile domainFile) {
|
||||||
|
if (!VTSession.class.isAssignableFrom(domainFile.getDomainObjectClass())) {
|
||||||
|
throw new IllegalArgumentException("File does not correspond to a VTSession");
|
||||||
|
}
|
||||||
|
if (!checkForUnSavedChanges()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!checkSessionFileAccess(domainFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VTSessionDB vtSessionDB = getVTSessionDB(domainFile, this);
|
||||||
|
if (vtSessionDB != null) {
|
||||||
|
try {
|
||||||
|
openVersionTrackingSession(vtSessionDB);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
vtSessionDB.release(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (CancelledException e) {
|
||||||
|
// ignore - return false
|
||||||
|
}
|
||||||
|
catch (VersionException e) {
|
||||||
|
VersionExceptionHandler.showVersionError(null, domainFile.getName(), "VT Session",
|
||||||
|
"open", e);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
Msg.showError(this, null, "Can't open VT Session: " + domainFile.getName(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OpenVTSessionTask extends Task {
|
||||||
|
|
||||||
|
private final Object consumer;
|
||||||
|
private final DomainFile vtSessionFile;
|
||||||
|
|
||||||
|
Exception exception;
|
||||||
|
VTSessionDB vtSessionDB;
|
||||||
|
|
||||||
|
OpenVTSessionTask(DomainFile vtSessionFile, Object consumer) {
|
||||||
|
super("Opening VT Session", true, false, true, true);
|
||||||
|
this.vtSessionFile = vtSessionFile;
|
||||||
|
this.consumer = consumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
try {
|
||||||
|
vtSessionDB =
|
||||||
|
(VTSessionDB) vtSessionFile.getDomainObject(consumer, true, true, monitor);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VTSessionDB getVTSessionDB(DomainFile vtSessionFile, Object consumer)
|
||||||
|
throws IOException, VersionException, CancelledException {
|
||||||
|
|
||||||
|
OpenVTSessionTask task = new OpenVTSessionTask(vtSessionFile, consumer);
|
||||||
|
|
||||||
|
TaskLauncher.launch(task);
|
||||||
|
|
||||||
|
if (task.exception != null) {
|
||||||
|
if (task.exception instanceof CancelledException ce) {
|
||||||
|
throw ce;
|
||||||
|
}
|
||||||
|
if (task.exception instanceof VersionException ve) {
|
||||||
|
throw ve;
|
||||||
|
}
|
||||||
|
if (task.exception instanceof IOException ioe) {
|
||||||
|
throw ioe;
|
||||||
|
}
|
||||||
|
throw new IOException("VTSessionDB failure", task.exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.vtSessionDB;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void openVersionTrackingSession(VTSession newSession) {
|
public void openVersionTrackingSession(VTSession newSession) {
|
||||||
|
// FIXME: new session wizard should have handled existing session before starting -
|
||||||
|
// should be no need for this check
|
||||||
if (!checkForUnSavedChanges()) {
|
if (!checkForUnSavedChanges()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -595,44 +763,80 @@ public class VTControllerImpl
|
||||||
// Inner Classes
|
// Inner Classes
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
||||||
private class MyFolderListener extends DomainFolderListenerAdapter {
|
private void updateProgram(DomainFile file, boolean isSource) {
|
||||||
|
|
||||||
@Override
|
String type = isSource ? "Source" : "Destination";
|
||||||
public void domainFileObjectReplaced(DomainFile file, DomainObject oldObject) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special handling for when a file is checked-in. The existing program has be moved
|
|
||||||
* to a proxy file (no longer in the project) so that it can be closed and the program
|
|
||||||
* re-opened with the new version after the check-in merge.
|
|
||||||
*/
|
|
||||||
if (session == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (session.getSourceProgram() != oldObject &&
|
|
||||||
session.getDestinationProgram() != oldObject) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Program newProgram;
|
Program newProgram;
|
||||||
try {
|
try {
|
||||||
newProgram = (Program) file.getDomainObject(this, false, false, TaskMonitor.DUMMY);
|
newProgram = (Program) file.getDomainObject(this, false, false, TaskMonitor.DUMMY);
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
Msg.showError(this, getParentComponent(), "Error opening program " + file, e);
|
Msg.showError(this, getParentComponent(),
|
||||||
|
"Error opening VT " + type + " Program: " + file, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldObject == session.getSourceProgram()) {
|
if (isSource) {
|
||||||
session.updateSourceProgram(newProgram);
|
session.updateSourceProgram(newProgram);
|
||||||
}
|
}
|
||||||
else if (oldObject == session.getDestinationProgram()) {
|
else {
|
||||||
session.updateDestinationProgram(newProgram);
|
session.updateDestinationProgram(newProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List<DomainObjectChangeRecord> events = new ArrayList<DomainObjectChangeRecord>();
|
// List<DomainObjectChangeRecord> events = new ArrayList<DomainObjectChangeRecord>();
|
||||||
// events.add(new DomainObjectChangeRecord(DomainObjectEvent.RESTORED));
|
// events.add(new DomainObjectChangeRecord(DomainObjectEvent.RESTORED));
|
||||||
// domainObjectChanged(new DomainObjectChangedEvent(newProgram, events));
|
// domainObjectChanged(new DomainObjectChangedEvent(newProgram, events));
|
||||||
matchInfoFactory.clearCache();
|
matchInfoFactory.clearCache();
|
||||||
fireSessionChanged();
|
fireSessionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyFolderListener extends DomainFolderListenerAdapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void domainFileObjectReplaced(DomainFile file, DomainObject oldObject) {
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.getSourceProgram() == oldObject) {
|
||||||
|
updateProgram(file, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type;
|
||||||
|
if (session == oldObject) {
|
||||||
|
type = "VT Session";
|
||||||
|
}
|
||||||
|
else if (session.getDestinationProgram() == oldObject) {
|
||||||
|
if (VTSessionFileUtil.canUpdate(file)) {
|
||||||
|
updateProgram(file, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
type = "Destination Program";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session or destination program can no longer be saved to project so we
|
||||||
|
// have no choice but to close session.
|
||||||
|
|
||||||
|
// Since we are already in the Swing thread we need to delay closing so we do
|
||||||
|
// not continue to block the Swing thread and the checkin which is in progress.
|
||||||
|
// This allows the DomainFile checkin to complete its processing first.
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
|
||||||
|
Msg.showInfo(this, plugin.getTool().getToolFrame(), "Closing VT Session",
|
||||||
|
type + " checkin has forced session close.\n" +
|
||||||
|
"You will be prompted to save any other changes if needed, after which\n" +
|
||||||
|
"you may reopen the VT Session.");
|
||||||
|
|
||||||
|
closeVersionTrackingSession();
|
||||||
|
|
||||||
|
// NOTE: a future convenience could be added to attempt reopening of session
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OpenSessionTask extends Task {
|
private class OpenSessionTask extends Task {
|
||||||
|
|
|
@ -216,10 +216,10 @@ public class VTPlugin extends Plugin {
|
||||||
for (DomainFile domainFile : data) {
|
for (DomainFile domainFile : data) {
|
||||||
if (domainFile != null &&
|
if (domainFile != null &&
|
||||||
VTSession.class.isAssignableFrom(domainFile.getDomainObjectClass())) {
|
VTSession.class.isAssignableFrom(domainFile.getDomainObjectClass())) {
|
||||||
openVersionTrackingSession(domainFile);
|
return controller.openVersionTrackingSession(domainFile);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainFile programFile1 = null;
|
DomainFile programFile1 = null;
|
||||||
DomainFile programFile2 = null;
|
DomainFile programFile2 = null;
|
||||||
for (DomainFile domainFile : data) {
|
for (DomainFile domainFile : data) {
|
||||||
|
@ -249,10 +249,6 @@ public class VTPlugin extends Plugin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openVersionTrackingSession(DomainFile domainFile) {
|
|
||||||
controller.openVersionTrackingSession(domainFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void readConfigState(SaveState saveState) {
|
public void readConfigState(SaveState saveState) {
|
||||||
controller.readConfigState(saveState);
|
controller.readConfigState(saveState);
|
||||||
|
@ -274,20 +270,18 @@ public class VTPlugin extends Plugin {
|
||||||
@Override
|
@Override
|
||||||
public void readDataState(SaveState saveState) {
|
public void readDataState(SaveState saveState) {
|
||||||
String pathname = saveState.getString("PATHNAME", null);
|
String pathname = saveState.getString("PATHNAME", null);
|
||||||
String location = saveState.getString("PROJECT_LOCATION", null);
|
if (pathname == null) {
|
||||||
String projectName = saveState.getString("PROJECT_NAME", null);
|
|
||||||
if (location == null || projectName == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ProjectLocator url = new ProjectLocator(location, projectName);
|
Project project = tool.getProject();
|
||||||
|
if (project == null) {
|
||||||
ProjectData projectData = tool.getProject().getProjectData(url);
|
|
||||||
if (projectData == null) {
|
|
||||||
Msg.showError(this, tool.getToolFrame(), "File Not Found", "Could not find " + url);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ProjectData projectData = project.getProjectData();
|
||||||
DomainFile domainFile = projectData.getFile(pathname);
|
DomainFile domainFile = projectData.getFile(pathname);
|
||||||
|
if (domainFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
controller.openVersionTrackingSession(domainFile);
|
controller.openVersionTrackingSession(domainFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,21 +292,7 @@ public class VTPlugin extends Plugin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DomainFile domainFile = session.getDomainFile();
|
DomainFile domainFile = session.getDomainFile();
|
||||||
|
saveState.putString("PATHNAME", domainFile.getPathname());
|
||||||
String projectLocation = null;
|
|
||||||
String projectName = null;
|
|
||||||
String path = null;
|
|
||||||
ProjectLocator url = domainFile.getProjectLocator();
|
|
||||||
if (url != null) {
|
|
||||||
projectLocation = url.getLocation();
|
|
||||||
projectName = url.getName();
|
|
||||||
path = domainFile.getPathname();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveState.putString("PROJECT_LOCATION", projectLocation);
|
|
||||||
saveState.putString("PROJECT_NAME", projectName);
|
|
||||||
saveState.putString("PATHNAME", path);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* ###
|
/* ###
|
||||||
* IP: GHIDRA
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,10 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.gui.wizard;
|
package ghidra.feature.vt.gui.wizard;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import docking.wizard.WizardState;
|
||||||
import ghidra.feature.vt.api.db.VTSessionDB;
|
import ghidra.feature.vt.api.db.VTSessionDB;
|
||||||
import ghidra.feature.vt.api.main.VTSession;
|
|
||||||
import ghidra.feature.vt.gui.plugin.VTController;
|
import ghidra.feature.vt.gui.plugin.VTController;
|
||||||
import ghidra.framework.data.DomainObjectAdapterDB;
|
|
||||||
import ghidra.framework.model.DomainFolder;
|
import ghidra.framework.model.DomainFolder;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.InvalidNameException;
|
import ghidra.util.InvalidNameException;
|
||||||
|
@ -28,11 +28,6 @@ import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.Task;
|
import ghidra.util.task.Task;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
import java.awt.EventQueue;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import docking.wizard.WizardState;
|
|
||||||
|
|
||||||
public class CreateNewSessionTask extends Task {
|
public class CreateNewSessionTask extends Task {
|
||||||
private final WizardState<VTWizardStateKey> state;
|
private final WizardState<VTWizardStateKey> state;
|
||||||
private final VTController controller;
|
private final VTController controller;
|
||||||
|
@ -45,57 +40,41 @@ public class CreateNewSessionTask extends Task {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(TaskMonitor monitor) {
|
public void run(TaskMonitor monitor) {
|
||||||
VTSession session = null;
|
VTSessionDB session = null;
|
||||||
String name = null;
|
String name = null;
|
||||||
try {
|
try {
|
||||||
Program sourceProgram = (Program) state.get(VTWizardStateKey.SOURCE_PROGRAM);
|
Program sourceProgram = (Program) state.get(VTWizardStateKey.SOURCE_PROGRAM);
|
||||||
Program destinationProgram = (Program) state.get(VTWizardStateKey.DESTINATION_PROGRAM);
|
Program destinationProgram = (Program) state.get(VTWizardStateKey.DESTINATION_PROGRAM);
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB("New Session", sourceProgram, destinationProgram, this);
|
||||||
VTSessionDB.createVTSession("New Session", sourceProgram, destinationProgram, this);
|
|
||||||
|
|
||||||
DomainObjectAdapterDB dobj = null;
|
|
||||||
if (session instanceof DomainObjectAdapterDB) {
|
|
||||||
dobj = (DomainObjectAdapterDB) session;
|
|
||||||
}
|
|
||||||
sourceProgram.release(controller.getTool());
|
sourceProgram.release(controller.getTool());
|
||||||
destinationProgram.release(controller.getTool());
|
destinationProgram.release(controller.getTool());
|
||||||
if (dobj != null) {
|
|
||||||
name = (String) state.get(VTWizardStateKey.SESSION_NAME);
|
name = (String) state.get(VTWizardStateKey.SESSION_NAME);
|
||||||
DomainFolder folder = (DomainFolder) state.get(VTWizardStateKey.NEW_SESSION_FOLDER);
|
DomainFolder folder = (DomainFolder) state.get(VTWizardStateKey.NEW_SESSION_FOLDER);
|
||||||
try {
|
try {
|
||||||
folder.createFile(name, dobj, monitor);
|
folder.createFile(name, session, monitor);
|
||||||
}
|
}
|
||||||
catch (InvalidNameException e) {
|
catch (InvalidNameException e) {
|
||||||
Msg.showError(this, null, "Invalid Domain Object Name",
|
Msg.showError(this, null, "Invalid Domain Object Name",
|
||||||
"Please report this error; the name should have been checked already");
|
"Please report this error; the name should have been checked already");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
final VTSession finalSession = session;
|
controller.openVersionTrackingSession(session);
|
||||||
EventQueue.invokeLater(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
controller.openVersionTrackingSession(finalSession);
|
|
||||||
releaseDomainObject(finalSession);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (CancelledException e) {
|
catch (CancelledException e) {
|
||||||
// the user cancelled; just cleanup
|
// ignore
|
||||||
releaseDomainObject(session);
|
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
releaseDomainObject(session);
|
Msg.showError(this, null, "Failed to Create Session",
|
||||||
Msg.showError(this, null, "Failed to Create Session", "Failed to create db file: " +
|
"Failed to create db file: " + name, e);
|
||||||
name, e);
|
}
|
||||||
|
finally {
|
||||||
|
if (session != null) {
|
||||||
|
session.release(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseDomainObject(VTSession session) {
|
|
||||||
if (session == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
((VTSessionDB) session).release(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,22 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.feature.vt.gui.wizard;
|
package ghidra.feature.vt.gui.wizard;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
import java.awt.GridBagConstraints;
|
import java.util.*;
|
||||||
import java.awt.GridBagLayout;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.*;
|
||||||
import javax.swing.Box;
|
|
||||||
import javax.swing.Icon;
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.JSeparator;
|
|
||||||
import javax.swing.JTextField;
|
|
||||||
import javax.swing.SwingConstants;
|
|
||||||
import javax.swing.event.DocumentEvent;
|
import javax.swing.event.DocumentEvent;
|
||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
|
|
||||||
|
@ -38,22 +26,19 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import docking.widgets.button.BrowseButton;
|
import docking.widgets.button.BrowseButton;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
import docking.wizard.AbstractMageJPanel;
|
import docking.wizard.*;
|
||||||
import docking.wizard.WizardPanelDisplayability;
|
|
||||||
import docking.wizard.WizardState;
|
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import generic.theme.GThemeDefaults.Ids.Fonts;
|
import generic.theme.GThemeDefaults.Ids.Fonts;
|
||||||
import generic.theme.Gui;
|
import generic.theme.Gui;
|
||||||
import ghidra.app.util.task.OpenProgramRequest;
|
import ghidra.app.util.task.OpenProgramRequest;
|
||||||
import ghidra.app.util.task.OpenProgramTask;
|
import ghidra.app.util.task.OpenProgramTask;
|
||||||
|
import ghidra.feature.vt.api.util.VTSessionFileUtil;
|
||||||
import ghidra.framework.main.DataTreeDialog;
|
import ghidra.framework.main.DataTreeDialog;
|
||||||
import ghidra.framework.model.DomainFile;
|
import ghidra.framework.model.DomainFile;
|
||||||
import ghidra.framework.model.DomainFolder;
|
import ghidra.framework.model.DomainFolder;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.*;
|
||||||
import ghidra.util.InvalidNameException;
|
|
||||||
import ghidra.util.StringUtilities;
|
|
||||||
import ghidra.util.task.TaskLauncher;
|
import ghidra.util.task.TaskLauncher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -309,28 +294,31 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
|
||||||
private String createVTSessionName(String sourceName, String destinationName) {
|
private String createVTSessionName(String sourceName, String destinationName) {
|
||||||
|
|
||||||
// if together they are within the bounds just return session name with both full names
|
// if together they are within the bounds just return session name with both full names
|
||||||
if (sourceName.length() + destinationName.length() <= 2 * VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
if (sourceName.length() + destinationName.length() <= 2 *
|
||||||
|
VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
||||||
return "VT_" + sourceName + "_" + destinationName;
|
return "VT_" + sourceName + "_" + destinationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// give destination name all space not used by source name
|
// give destination name all space not used by source name
|
||||||
if (sourceName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
if (sourceName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
||||||
int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - sourceName.length();
|
int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - sourceName.length();
|
||||||
destinationName =
|
destinationName = StringUtilities.trimMiddle(destinationName,
|
||||||
StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover);
|
VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover);
|
||||||
return "VT_" + sourceName + "_" + destinationName;
|
return "VT_" + sourceName + "_" + destinationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// give source name all space not used by destination name
|
// give source name all space not used by destination name
|
||||||
if (destinationName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
if (destinationName.length() < VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH) {
|
||||||
int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - destinationName.length();
|
int leftover = VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH - destinationName.length();
|
||||||
sourceName = StringUtilities.trimMiddle(sourceName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover);
|
sourceName = StringUtilities.trimMiddle(sourceName,
|
||||||
|
VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH + leftover);
|
||||||
return "VT_" + sourceName + "_" + destinationName;
|
return "VT_" + sourceName + "_" + destinationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both too long, shorten both of them
|
// if both too long, shorten both of them
|
||||||
sourceName = StringUtilities.trimMiddle(sourceName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH);
|
sourceName = StringUtilities.trimMiddle(sourceName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH);
|
||||||
destinationName = StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH);
|
destinationName =
|
||||||
|
StringUtilities.trimMiddle(destinationName, VTSESSION_NAME_PROGRAM_NAME_MAX_LENGTH);
|
||||||
|
|
||||||
return "VT_" + sourceName + "_" + destinationName;
|
return "VT_" + sourceName + "_" + destinationName;
|
||||||
}
|
}
|
||||||
|
@ -418,16 +406,17 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
|
||||||
state.put(VTWizardStateKey.NEW_SESSION_FOLDER, folder);
|
state.put(VTWizardStateKey.NEW_SESSION_FOLDER, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openProgram(ProgramInfo programInfo) {
|
private boolean openProgram(ProgramInfo programInfo) {
|
||||||
|
|
||||||
if (programInfo.hasProgram()) {
|
if (programInfo.hasProgram()) {
|
||||||
return; // already open
|
return true; // already open
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool);
|
OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool);
|
||||||
new TaskLauncher(openProgramTask, tool.getActiveWindow());
|
new TaskLauncher(openProgramTask, tool.getActiveWindow());
|
||||||
OpenProgramRequest openProgram = openProgramTask.getOpenProgram();
|
OpenProgramRequest openProgram = openProgramTask.getOpenProgram();
|
||||||
programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null);
|
programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null);
|
||||||
|
return programInfo.hasProgram();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -480,19 +469,25 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
|
||||||
DomainFile file = folder.getFile(name);
|
DomainFile file = folder.getFile(name);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
notifyListenersOfStatusMessage(
|
notifyListenersOfStatusMessage(
|
||||||
"'" + file.getPathname() + "' is the name of an existing domain file");
|
"'" + file.getPathname() + "' is the name of an existing project file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
openProgram(sourceProgramInfo);
|
// Known Issue: Opening programs before comitted to using them (i.e., Next is clicked) seems
|
||||||
if (!sourceProgramInfo.hasProgram()) {
|
// premature and will subject user to prompts about possible checkout and/or upgrades
|
||||||
|
// with possible slow re-disassembly (see GP-4151)
|
||||||
|
|
||||||
|
if (!isValidDestinationProgramFile() || !isValidSourceProgramFile()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!openProgram(sourceProgramInfo)) {
|
||||||
notifyListenersOfStatusMessage(
|
notifyListenersOfStatusMessage(
|
||||||
"Can't open source program " + sourceProgramInfo.getName());
|
"Can't open source program " + sourceProgramInfo.getName());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
openProgram(destinationProgramInfo);
|
if (!openProgram(destinationProgramInfo)) {
|
||||||
if (!destinationProgramInfo.hasProgram()) {
|
|
||||||
notifyListenersOfStatusMessage(
|
notifyListenersOfStatusMessage(
|
||||||
"Can't open destination program " + destinationProgramInfo.getName());
|
"Can't open destination program " + destinationProgramInfo.getName());
|
||||||
return false;
|
return false;
|
||||||
|
@ -502,6 +497,29 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidSourceProgramFile() {
|
||||||
|
try {
|
||||||
|
VTSessionFileUtil.validateSourceProgramFile(sourceProgramInfo.file, false);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
notifyListenersOfStatusMessage(e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidDestinationProgramFile() {
|
||||||
|
try {
|
||||||
|
VTSessionFileUtil.validateDestinationProgramFile(destinationProgramInfo.file, false,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
notifyListenersOfStatusMessage(e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addDependencies(WizardState<VTWizardStateKey> state) {
|
public void addDependencies(WizardState<VTWizardStateKey> state) {
|
||||||
// none
|
// none
|
||||||
|
|
|
@ -44,8 +44,7 @@ public class VTNewSessionWizardManager extends AbstractMagePanelManager<VTWizard
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<MagePanel<VTWizardStateKey>> createPanels() {
|
protected List<MagePanel<VTWizardStateKey>> createPanels() {
|
||||||
List<MagePanel<VTWizardStateKey>> panels =
|
List<MagePanel<VTWizardStateKey>> panels = new ArrayList<>();
|
||||||
new ArrayList<>();
|
|
||||||
panels.add(new NewSessionPanel(controller.getTool()));
|
panels.add(new NewSessionPanel(controller.getTool()));
|
||||||
panels.add(new PreconditionsPanel(this));
|
panels.add(new PreconditionsPanel(this));
|
||||||
panels.add(new SummaryPanel());
|
panels.add(new SummaryPanel());
|
||||||
|
|
|
@ -115,8 +115,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionNoSelectionUnlimitedAddresses() throws Exception {
|
public void testAddToSessionNoSelectionUnlimitedAddresses() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -170,8 +169,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionNoSelectionLimitAddressesToEntireProgram() throws Exception {
|
public void testAddToSessionNoSelectionLimitAddressesToEntireProgram() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -231,8 +229,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionNoSelectionLimitAddressesToMyOwn() throws Exception {
|
public void testAddToSessionNoSelectionLimitAddressesToMyOwn() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -292,8 +289,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionNoSelectionLimitAddressesToMyOwnChanged() throws Exception {
|
public void testAddToSessionNoSelectionLimitAddressesToMyOwnChanged() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -366,8 +362,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionWithSelectionLimitAddressesToEntireProgram() throws Exception {
|
public void testAddToSessionWithSelectionLimitAddressesToEntireProgram() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -429,8 +424,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionWithSelectionLimitAddressesToSelection() throws Exception {
|
public void testAddToSessionWithSelectionLimitAddressesToSelection() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -492,8 +486,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionWithSelectionLimitAddressesToMyOwn() throws Exception {
|
public void testAddToSessionWithSelectionLimitAddressesToMyOwn() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -568,8 +561,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAddToSessionWithSelectionLimitAddressesToMyOwnThenBackNext() throws Exception {
|
public void testAddToSessionWithSelectionLimitAddressesToMyOwnThenBackNext() throws Exception {
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
@ -671,8 +663,7 @@ public class VTAddToSessionTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
public void testAddToSessionResultingInNoMatchesFound() throws Exception {
|
public void testAddToSessionResultingInNoMatchesFound() throws Exception {
|
||||||
|
|
||||||
setErrorGUIEnabled(true);
|
setErrorGUIEnabled(true);
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
String sessionName = "Untitled";
|
String sessionName = "Untitled";
|
||||||
|
|
|
@ -78,8 +78,7 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
plugin = getPlugin(tool, VTPlugin.class);
|
plugin = getPlugin(tool, VTPlugin.class);
|
||||||
controller = new VTControllerImpl(plugin);
|
controller = new VTControllerImpl(plugin);
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||||
|
|
|
@ -81,8 +81,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
|
||||||
VTPlugin plugin = getPlugin(tool, VTPlugin.class);
|
VTPlugin plugin = getPlugin(tool, VTPlugin.class);
|
||||||
controller = new VTControllerImpl(plugin);
|
controller = new VTControllerImpl(plugin);
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||||
|
@ -390,8 +389,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testApplyMatch_ReplaceSignature_CustomSourceAndDest()
|
public void testApplyMatch_ReplaceSignature_CustomSourceAndDest() throws Exception {
|
||||||
throws Exception {
|
|
||||||
|
|
||||||
useMatch("0x00401040", "0x00401040");
|
useMatch("0x00401040", "0x00401040");
|
||||||
|
|
||||||
|
@ -442,8 +440,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testApplyMatch_ReplaceSignature_NormalSourceCustomDest()
|
public void testApplyMatch_ReplaceSignature_NormalSourceCustomDest() throws Exception {
|
||||||
throws Exception {
|
|
||||||
|
|
||||||
useMatch("0x00401040", "0x00401040");
|
useMatch("0x00401040", "0x00401040");
|
||||||
|
|
||||||
|
@ -666,8 +663,7 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
|
||||||
|
|
||||||
env.release(destinationProgram);
|
env.release(destinationProgram);
|
||||||
destinationProgram = createToyDestinationProgram();// env.getProgram("helloProgram"); // get a program without cdecl
|
destinationProgram = createToyDestinationProgram();// env.getProgram("helloProgram"); // get a program without cdecl
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||||
|
|
||||||
|
|
|
@ -94,8 +94,7 @@ public class VTMatchApplyTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
plugin = getPlugin(tool, VTPlugin.class);
|
plugin = getPlugin(tool, VTPlugin.class);
|
||||||
controller = new VTControllerImpl(plugin);
|
controller = new VTControllerImpl(plugin);
|
||||||
|
|
||||||
session =
|
session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
|
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session));
|
runSwing(() -> controller.openVersionTrackingSession(session));
|
||||||
|
|
|
@ -79,8 +79,7 @@ public abstract class AbstractCorrelatorTest extends AbstractGhidraHeadedIntegra
|
||||||
protected void exerciseFunctionsForFactory(final VTProgramCorrelatorFactory factory,
|
protected void exerciseFunctionsForFactory(final VTProgramCorrelatorFactory factory,
|
||||||
AddressSetView sourceSetThatShouldBeFound) throws Exception {
|
AddressSetView sourceSetThatShouldBeFound) throws Exception {
|
||||||
String name = factory.getName();
|
String name = factory.getName();
|
||||||
VTSession session =
|
VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this);
|
||||||
VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int sessionTransaction = session.startTransaction(name);
|
int sessionTransaction = session.startTransaction(name);
|
||||||
|
@ -145,8 +144,7 @@ public abstract class AbstractCorrelatorTest extends AbstractGhidraHeadedIntegra
|
||||||
protected void exercisePreciseMatchesForFactory(VTProgramCorrelatorFactory factory,
|
protected void exercisePreciseMatchesForFactory(VTProgramCorrelatorFactory factory,
|
||||||
Map<Address, Address> map) throws Exception {
|
Map<Address, Address> map) throws Exception {
|
||||||
String name = factory.getName();
|
String name = factory.getName();
|
||||||
VTSession session =
|
VTSession session = new VTSessionDB(name, sourceProgram, destinationProgram, this);
|
||||||
VTSessionDB.createVTSession(name, sourceProgram, destinationProgram, this);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int sessionTransaction = session.startTransaction(name);
|
int sessionTransaction = session.startTransaction(name);
|
||||||
|
|
|
@ -348,7 +348,7 @@ public abstract class AbstractVTMarkupItemTest extends AbstractGhidraHeadedInteg
|
||||||
}
|
}
|
||||||
|
|
||||||
protected VTSessionDB createNewSession() throws Exception {
|
protected VTSessionDB createNewSession() throws Exception {
|
||||||
return VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
|
return new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager",
|
||||||
sourceProgram, destinationProgram, this);
|
sourceProgram, destinationProgram, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,8 +81,7 @@ public class VTBaseTestCase extends AbstractGenericTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
public VTSessionDB createVTSession() throws IOException {
|
public VTSessionDB createVTSession() throws IOException {
|
||||||
return VTSessionDB.createVTSession("Test DB", sourceProgram, destinationProgram,
|
return new VTSessionDB("Test DB", sourceProgram, destinationProgram, VTTestUtils.class);
|
||||||
VTTestUtils.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getRandomInt() {
|
public static int getRandomInt() {
|
||||||
|
|
|
@ -69,7 +69,7 @@ public class VTTestEnv extends TestEnv {
|
||||||
sourceProgram = getProgram(sourceProgramName);
|
sourceProgram = getProgram(sourceProgramName);
|
||||||
destinationProgram = getProgram(destinationProgramName);
|
destinationProgram = getProgram(destinationProgramName);
|
||||||
|
|
||||||
session = VTSessionDB.createVTSession("Test", sourceProgram, destinationProgram, getTool());
|
session = new VTSessionDB("Test", sourceProgram, destinationProgram, getTool());
|
||||||
|
|
||||||
VTProgramCorrelator correlator = factory.createCorrelator(sourceProgram,
|
VTProgramCorrelator correlator = factory.createCorrelator(sourceProgram,
|
||||||
sourceProgram.getMemory(), destinationProgram, destinationProgram.getMemory(), null);
|
sourceProgram.getMemory(), destinationProgram, destinationProgram.getMemory(), null);
|
||||||
|
@ -111,7 +111,7 @@ public class VTTestEnv extends TestEnv {
|
||||||
}
|
}
|
||||||
|
|
||||||
private VTSessionDB createAndOpenVTSession() throws IOException {
|
private VTSessionDB createAndOpenVTSession() throws IOException {
|
||||||
session = VTSessionDB.createVTSession("Test", sourceProgram, destinationProgram, getTool());
|
session = new VTSessionDB("Test", sourceProgram, destinationProgram, getTool());
|
||||||
|
|
||||||
runSwing(() -> controller.openVersionTrackingSession(session), false);
|
runSwing(() -> controller.openVersionTrackingSession(session), false);
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class StubVTController implements VTController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void openVersionTrackingSession(DomainFile domainFile) {
|
public boolean openVersionTrackingSession(DomainFile domainFile) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,8 @@ public abstract class Transaction implements AutoCloseable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End this transaction if currently active.
|
* End this transaction if currently active.
|
||||||
* @param commit true if changes shuold be commited, false if all changes in this transaction
|
* @param commit true if changes should be commited, false if all changes in this transaction
|
||||||
* shuold be discarded (i.e., rollback). If this is a "sub-transaction" and commit is false,
|
* should be discarded (i.e., rollback). If this is a "sub-transaction" and commit is false,
|
||||||
* the larger transaction will rollback upon completion.
|
* the larger transaction will rollback upon completion.
|
||||||
* @return true if changes have been commited or false if nothing to commit or commit parameter
|
* @return true if changes have been commited or false if nothing to commit or commit parameter
|
||||||
* was specified as false.
|
* was specified as false.
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class DomainFileProxy implements DomainFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainFileProxy(String name, String parentPath, DomainObjectAdapter doa, int version,
|
DomainFileProxy(String name, String parentPath, DomainObjectAdapter doa, int version,
|
||||||
String fileID, ProjectLocator projectLocation) {
|
String fileID, ProjectLocator projectLocation) throws IOException {
|
||||||
|
|
||||||
this(name, doa);
|
this(name, doa);
|
||||||
this.parentPath = parentPath;
|
this.parentPath = parentPath;
|
||||||
|
|
|
@ -94,7 +94,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||||
consumers = new ArrayList<Object>();
|
consumers = new ArrayList<Object>();
|
||||||
consumers.add(consumer);
|
consumers.add(consumer);
|
||||||
if (!UserData.class.isAssignableFrom(getClass())) {
|
if (!UserData.class.isAssignableFrom(getClass())) {
|
||||||
// UserData instances do not utilize DomainFile storage
|
|
||||||
domainFile = new DomainFileProxy(name, this);
|
domainFile = new DomainFileProxy(name, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +184,12 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||||
return temporary;
|
return temporary;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setDomainFile(DomainFile df) {
|
/**
|
||||||
|
* Set the {@link DomainFile} associated with this instance.
|
||||||
|
* @param df domain file
|
||||||
|
* @throws DomainObjectException if a severe failure occurs during the operation.
|
||||||
|
*/
|
||||||
|
protected void setDomainFile(DomainFile df) throws DomainObjectException {
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
throw new IllegalArgumentException("DomainFile must not be null");
|
throw new IllegalArgumentException("DomainFile must not be null");
|
||||||
}
|
}
|
||||||
|
@ -197,7 +201,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||||
domainFile = df;
|
domainFile = df;
|
||||||
fireEvent(new DomainObjectChangeRecord(DomainObjectEvent.FILE_CHANGED, oldDf, df));
|
fireEvent(new DomainObjectChangeRecord(DomainObjectEvent.FILE_CHANGED, oldDf, df));
|
||||||
fileChangeListeners.invoke().domainFileChanged(this);
|
fileChangeListeners.invoke().domainFileChanged(this);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void close() {
|
protected void close() {
|
||||||
|
|
|
@ -528,6 +528,9 @@ public class GhidraFileData {
|
||||||
projectData.clearDomainObject(getPathname());
|
projectData.clearDomainObject(getPathname());
|
||||||
// generate IOException
|
// generate IOException
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
|
if (cause == null) {
|
||||||
|
cause = e;
|
||||||
|
}
|
||||||
if (cause instanceof IOException) {
|
if (cause instanceof IOException) {
|
||||||
throw (IOException) cause;
|
throw (IOException) cause;
|
||||||
}
|
}
|
||||||
|
@ -831,9 +834,12 @@ public class GhidraFileData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the object is read-only. From a framework point of view a read-only object
|
* Returns whether this file is explicitly marked as read-only. This method is only supported
|
||||||
* can never be changed.
|
* by the local file system and does not apply to a versioned file that is not checked-out.
|
||||||
* @return true if read-only
|
* A versioned file that is not checked-out will always return false, while a
|
||||||
|
* {@link DomainFileProxy} will always return true.
|
||||||
|
* From a framework point of view a read-only file can never be changed.
|
||||||
|
* @return true if this file is marked read-only
|
||||||
*/
|
*/
|
||||||
boolean isReadOnly() {
|
boolean isReadOnly() {
|
||||||
synchronized (fileSystem) {
|
synchronized (fileSystem) {
|
||||||
|
|
|
@ -158,7 +158,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
|
||||||
toolFrame.addWindowListener(windowListener);
|
toolFrame.addWindowListener(windowListener);
|
||||||
|
|
||||||
AppInfo.setFrontEndTool(this);
|
AppInfo.setFrontEndTool(this);
|
||||||
AppInfo.setActiveProject(getProject());
|
|
||||||
|
|
||||||
initFrontEndOptions();
|
initFrontEndOptions();
|
||||||
}
|
}
|
||||||
|
@ -408,7 +407,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
|
||||||
|
|
||||||
configureToolAction.setEnabled(true);
|
configureToolAction.setEnabled(true);
|
||||||
setProject(project);
|
setProject(project);
|
||||||
AppInfo.setActiveProject(project);
|
|
||||||
plugin.setActiveProject(project);
|
plugin.setActiveProject(project);
|
||||||
firePluginEvent(new ProjectPluginEvent(getClass().getSimpleName(), project));
|
firePluginEvent(new ProjectPluginEvent(getClass().getSimpleName(), project));
|
||||||
}
|
}
|
||||||
|
@ -616,7 +614,6 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
|
||||||
|
|
||||||
// Treat setVisible(false) as a dispose, as this is the only time we should be hidden
|
// Treat setVisible(false) as a dispose, as this is the only time we should be hidden
|
||||||
AppInfo.setFrontEndTool(null);
|
AppInfo.setFrontEndTool(null);
|
||||||
AppInfo.setActiveProject(null);
|
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -645,9 +642,8 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
|
||||||
return isConfigurable();
|
return isConfigurable();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
MenuData menuData =
|
MenuData menuData = new MenuData(
|
||||||
new MenuData(new String[] { ToolConstants.MENU_FILE, "Install Extensions" }, null,
|
new String[] { ToolConstants.MENU_FILE, "Install Extensions" }, null, CONFIGURE_GROUP);
|
||||||
CONFIGURE_GROUP);
|
|
||||||
menuData.setMenuSubGroup(CONFIGURE_GROUP + 2);
|
menuData.setMenuSubGroup(CONFIGURE_GROUP + 2);
|
||||||
installExtensionsAction.setMenuBarData(menuData);
|
installExtensionsAction.setMenuBarData(menuData);
|
||||||
|
|
||||||
|
|
|
@ -331,9 +331,12 @@ public interface DomainFile extends Comparable<DomainFile> {
|
||||||
public void setReadOnly(boolean state) throws IOException;
|
public void setReadOnly(boolean state) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the object is read-only. From a framework point of view a read-only object
|
* Returns whether this file is explicitly marked as read-only. This method is only supported
|
||||||
* can never be changed.
|
* by the local file system and does not apply to a versioned file that is not checked-out.
|
||||||
* @return true if read-only
|
* A versioned file that is not checked-out will always return false, while a
|
||||||
|
* {@link DomainFileProxy} will always return true.
|
||||||
|
* From a framework point of view a read-only file can never be changed.
|
||||||
|
* @return true if this file is marked read-only
|
||||||
*/
|
*/
|
||||||
public boolean isReadOnly();
|
public boolean isReadOnly();
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.jdom.output.XMLOutputter;
|
||||||
import ghidra.framework.client.RepositoryAdapter;
|
import ghidra.framework.client.RepositoryAdapter;
|
||||||
import ghidra.framework.data.DefaultProjectData;
|
import ghidra.framework.data.DefaultProjectData;
|
||||||
import ghidra.framework.data.TransientDataManager;
|
import ghidra.framework.data.TransientDataManager;
|
||||||
|
import ghidra.framework.main.AppInfo;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.options.SaveState;
|
import ghidra.framework.options.SaveState;
|
||||||
import ghidra.framework.project.tool.GhidraToolTemplate;
|
import ghidra.framework.project.tool.GhidraToolTemplate;
|
||||||
|
@ -291,16 +292,16 @@ public class DefaultProject implements Project {
|
||||||
throw new IOException("Invalid Ghidra URL specified: " + url);
|
throw new IOException("Invalid Ghidra URL specified: " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectData projectData = otherViewsMap.get(url);
|
ProjectData viewedProjectData = otherViewsMap.get(url);
|
||||||
if (projectData == null) {
|
if (viewedProjectData == null) {
|
||||||
projectData = openProjectView(url);
|
viewedProjectData = openProjectView(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectData != null && visible && visibleViews.add(url)) {
|
if (viewedProjectData != null && visible && visibleViews.add(url)) {
|
||||||
notifyVisibleViewAdded(url);
|
notifyVisibleViewAdded(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectData;
|
return viewedProjectData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,6 +379,11 @@ public class DefaultProject implements Project {
|
||||||
synchronized (otherViewsMap) {
|
synchronized (otherViewsMap) {
|
||||||
isClosed = true;
|
isClosed = true;
|
||||||
|
|
||||||
|
// Clear active project if this is the current active project.
|
||||||
|
if (AppInfo.getActiveProject() == this) {
|
||||||
|
AppInfo.setActiveProject(null);
|
||||||
|
}
|
||||||
|
|
||||||
for (DefaultProjectData dataMgr : otherViewsMap.values()) {
|
for (DefaultProjectData dataMgr : otherViewsMap.values()) {
|
||||||
if (dataMgr != null) {
|
if (dataMgr != null) {
|
||||||
dataMgr.close();
|
dataMgr.close();
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ghidra.framework.GenericRunInfo;
|
||||||
import ghidra.framework.ToolUtils;
|
import ghidra.framework.ToolUtils;
|
||||||
import ghidra.framework.client.*;
|
import ghidra.framework.client.*;
|
||||||
import ghidra.framework.data.TransientDataManager;
|
import ghidra.framework.data.TransientDataManager;
|
||||||
|
import ghidra.framework.main.AppInfo;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.preferences.Preferences;
|
import ghidra.framework.preferences.Preferences;
|
||||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||||
|
@ -111,6 +112,8 @@ public class DefaultProjectManager implements ProjectManager {
|
||||||
lastOpenedProject = projectLocator;
|
lastOpenedProject = projectLocator;
|
||||||
updatePreferences();
|
updatePreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppInfo.setActiveProject(currentProject);
|
||||||
return currentProject;
|
return currentProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +141,7 @@ public class DefaultProjectManager implements ProjectManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentProject = new DefaultProject(this, projectLocator, resetOwner);
|
currentProject = new DefaultProject(this, projectLocator, resetOwner);
|
||||||
|
AppInfo.setActiveProject(currentProject);
|
||||||
if (doRestore) {
|
if (doRestore) {
|
||||||
currentProject.restore();
|
currentProject.restore();
|
||||||
}
|
}
|
||||||
|
@ -166,6 +170,7 @@ public class DefaultProjectManager implements ProjectManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppInfo.setActiveProject(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue