GP-6402 Improved project name/path error checking. Added support for

Windows UNC project paths. Refactor of GhidraURL utility methods and URI
use.  Other related cleanup.
This commit is contained in:
ghidra1
2026-02-04 19:21:38 -05:00
parent 0ee235fba7
commit 13bb1e8005
59 changed files with 2260 additions and 1685 deletions
@@ -44,6 +44,7 @@ import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.Msg;
import ghidra.util.NotOwnerException;
import ghidra.util.exception.NotFoundException;
public class DebuggerCoordinates {
@@ -68,8 +69,7 @@ public class DebuggerCoordinates {
private static final String KEY_FRAME = "Frame";
private static final String KEY_OBJ_PATH = "ObjectPath";
public static boolean equalsIgnoreTargetAndView(DebuggerCoordinates a,
DebuggerCoordinates b) {
public static boolean equalsIgnoreTargetAndView(DebuggerCoordinates a, DebuggerCoordinates b) {
if (!Objects.equals(a.trace, b.trace)) {
return false;
}
@@ -124,8 +124,8 @@ public class DebuggerCoordinates {
@Override
public String toString() {
return String.format(
"Coords(trace=%s,target=%s,thread=%s,view=%s,time=%s,frame=%d,path=%s)",
trace, target, thread, view, time, frame, path);
"Coords(trace=%s,target=%s,thread=%s,view=%s,time=%s,frame=%d,path=%s)", trace, target,
thread, view, time, frame, path);
}
@Override
@@ -244,15 +244,14 @@ public class DebuggerCoordinates {
}
private static Integer resolveFrame(Target target, TraceThread thread, TraceSchedule time) {
if (target == null || target.getSnap() != time.getSnap() ||
!target.isSupportsFocus()) {
if (target == null || target.getSnap() != time.getSnap() || !target.isSupportsFocus()) {
return resolveFrame(thread, time);
}
return resolveFrame(target, target.getFocus());
}
private static KeyPath resolvePath(Target target, TraceThread thread,
Integer frame, TraceSchedule time) {
private static KeyPath resolvePath(Target target, TraceThread thread, Integer frame,
TraceSchedule time) {
if (target.getSnap() != time.getSnap() || !target.isSupportsFocus()) {
return resolvePath(target.getTrace(), thread, frame, time);
}
@@ -424,8 +423,8 @@ public class DebuggerCoordinates {
Integer newFrame = resolveFrame(newThread, newTime);
KeyPath threadOrFramePath = resolvePath(newThread, newFrame, newTime);
KeyPath newPath = choose(path, threadOrFramePath);
return new DebuggerCoordinates(trace, platform, target, newThread, view, newTime,
newFrame, newPath);
return new DebuggerCoordinates(trace, platform, target, newThread, view, newTime, newFrame,
newPath);
}
/**
@@ -478,8 +477,7 @@ public class DebuggerCoordinates {
}
private DebuggerCoordinates replaceView(TraceProgramView newView) {
return new DebuggerCoordinates(trace, platform, target, thread, newView, time, frame,
path);
return new DebuggerCoordinates(trace, platform, target, thread, newView, time, frame, path);
}
private static TraceSchedule resolveTime(TraceProgramView view) {
@@ -532,9 +530,8 @@ public class DebuggerCoordinates {
if (object == null) {
return null;
}
TraceStackFrame frame = object.queryCanonicalAncestorsInterface(TraceStackFrame.class)
.findFirst()
.orElse(null);
TraceStackFrame frame =
object.queryCanonicalAncestorsInterface(TraceStackFrame.class).findFirst().orElse(null);
return frame == null ? null : frame.getLevel();
}
@@ -549,15 +546,13 @@ public class DebuggerCoordinates {
return new DebuggerCoordinates(trace, platform, target, thread, view, time, frame,
newPath);
}
TraceThread newThread = target != null
? resolveThread(target, newPath)
: resolveThread(trace, newPath);
Integer newFrame = target != null
? resolveFrame(target, newPath)
: resolveFrame(trace, newPath);
TraceThread newThread =
target != null ? resolveThread(target, newPath) : resolveThread(trace, newPath);
Integer newFrame =
target != null ? resolveFrame(target, newPath) : resolveFrame(trace, newPath);
return new DebuggerCoordinates(trace, platform, target, newThread, view, time,
newFrame, newPath);
return new DebuggerCoordinates(trace, platform, target, newThread, view, time, newFrame,
newPath);
}
public DebuggerCoordinates pathNonCanonical(KeyPath newPath) {
@@ -747,7 +742,7 @@ public class DebuggerCoordinates {
"Not project owner: " + projLoc + "(" + pathname + ")");
return null;
}
catch (IOException | LockException e) {
catch (NotFoundException | IOException | LockException e) {
Msg.error(DebuggerCoordinates.class, "Project error: " + e.getMessage());
return null;
}
@@ -20,8 +20,7 @@ import static ghidra.program.util.ProgramEvent.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import java.util.*;
import java.util.List;
import java.util.function.IntSupplier;
@@ -462,10 +461,10 @@ public class BSimSearchResultsProvider extends ComponentProviderAdapter {
ProgramManager service = tool.getService(ProgramManager.class);
try {
URL url = new URL(urlString);
URL url = new URI(urlString).toURL();
return service.openProgram(url, ProgramManager.OPEN_CURRENT);
}
catch (MalformedURLException exc) {
catch (MalformedURLException | URISyntaxException exc) {
return null;
}
}
@@ -474,7 +473,7 @@ public class BSimSearchResultsProvider extends ComponentProviderAdapter {
ProgramManager service = tool.getService(ProgramManager.class);
try {
URL url = new URL(urlString);
URL url = new URI(urlString).toURL();
Program remote = service.openCachedProgram(url, this);
if (remote == null) {
return null;
@@ -486,7 +485,7 @@ public class BSimSearchResultsProvider extends ComponentProviderAdapter {
}
return remote;
}
catch (MalformedURLException exc) {
catch (MalformedURLException | URISyntaxException exc) {
return null;
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,8 +15,7 @@
*/
package ghidra.features.bsim.gui.search.results.apply;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import java.util.*;
import docking.DockingWindowManager;
@@ -199,9 +198,9 @@ public abstract class AbstractBSimApplyTask extends ProgramTask {
private URL getRemoteProgramURL(BSimMatchResult result) {
String urlString = result.getExecutableURLString();
try {
return new URL(urlString);
return new URI(urlString).toURL();
}
catch (MalformedURLException e) {
catch (MalformedURLException | URISyntaxException e) {
error("Bad URL: " + urlString, result);
}
return null;
@@ -98,7 +98,7 @@ public class BSimClientFactory {
// "ghidra://host/repo?service=bsim"
// String repositoryURL = "ghidra://" + ghidraURL.getAuthority() + "?service=bsim";
// Currenly all we do is assume that the BSim server is a PostgreSQL server
// Currently, all we do is assume that the BSim server is a PostgreSQL server
// on the same host and with the same repo name as the ghidra server
repositoryURL = "postgresql://" + url.getHost(); // Just use the hostname
}
@@ -65,7 +65,7 @@ public class BSimServerInfo implements Comparable<BSimServerInfo> {
* @param userinfo connection user info, {@code username[:password]} (ignored for {@link DBType#file}).
* If blank, {@link ClientUtil#getUserName()} is used.
* @param host host name (ignored for {@link DBType#file})
* @param port port number (ignored for {@link DBType#file})
* @param port port number (ignored for {@link DBType#file}, -1 for default)
* @param dbName name of database (simple database name except for {@link DBType#file}
* which should reflect an absolute file path. On Windows OS the path may start with a
* drive letter.
@@ -118,7 +118,7 @@ public class BSimServerInfo implements Comparable<BSimServerInfo> {
*
* @param dbType BSim DB type
* @param host host name (ignored for {@link DBType#file})
* @param port port number (ignored for {@link DBType#file})
* @param port port number (ignored for {@link DBType#file}, -1 for default)
* @param dbName name of database (simple database name except for {@link DBType#file}
* which should reflect an absolute file path. On Windows OS the path may start with a
* drive letter.
@@ -1,191 +0,0 @@
/* ###
* 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.features.bsim.query.client;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.*;
import org.xml.sax.*;
import generic.lsh.vector.LSHVectorFactory;
import ghidra.features.bsim.query.BSimServerInfo;
import ghidra.features.bsim.query.FunctionDatabase;
import ghidra.features.bsim.query.description.DatabaseInformation;
import ghidra.features.bsim.query.protocol.*;
import ghidra.framework.client.ClientUtil;
import ghidra.xml.NonThreadedXmlPullParserImpl;
import ghidra.xml.XmlPullParser;
public class FunctionDatabaseProxy implements FunctionDatabase {
private DatabaseInformation info;
private LSHVectorFactory vectorFactory;
private URL httpURL;
private BSimError lasterror;
private Status status;
private boolean isinit;
private XmlErrorHandler xmlErrorHandler;
static class XmlErrorHandler implements ErrorHandler {
@Override
public void warning(SAXParseException exception) throws SAXException {
// Ignore warnings
}
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
}
public FunctionDatabaseProxy(URL url) throws MalformedURLException {
httpURL = new URL(url.toString()); // Make sure URL has a real handler
lasterror = null;
info = null;
vectorFactory = FunctionDatabase.generateLSHVectorFactory();
status = Status.Unconnected;
isinit = false;
xmlErrorHandler = new XmlErrorHandler();
}
@Override
public Status getStatus() {
return status;
}
@Override
public ConnectionType getConnectionType() {
return ConnectionType.Unencrypted_No_Authentication;
}
@Override
public String getUserName() {
return ClientUtil.getUserName();
}
@Override
public LSHVectorFactory getLSHVectorFactory() {
return vectorFactory;
}
@Override
public DatabaseInformation getInfo() {
return info;
}
@Override
public int compareLayout() {
if (info.layout_version == PostgresFunctionDatabase.LAYOUT_VERSION) {
return 0;
}
return (info.layout_version < PostgresFunctionDatabase.LAYOUT_VERSION) ? -1 : 1;
}
@Override
public String getURLString() {
return httpURL.toString();
}
@Override
public BSimServerInfo getServerInfo() {
return new BSimServerInfo(httpURL);
}
@Override
public boolean initialize() {
if (isinit) {
return true;
}
if (httpURL == null) {
status = Status.Error;
lasterror =
new FunctionDatabase.BSimError(ErrorCategory.Initialization, "MalformedURL");
return false;
}
QueryInfo queryInfo = new QueryInfo();
QueryResponseRecord response = query(queryInfo);
if (response == null) {
return false;
}
info = ((ResponseInfo) response).info;
status = Status.Ready;
isinit = true;
return true;
}
@Override
public void close() {
status = Status.Unconnected;
isinit = false;
info = null;
}
@Override
public BSimError getLastError() {
return lasterror;
}
@Override
public QueryResponseRecord query(BSimQuery<?> query) {
HttpURLConnection connection;
query.buildResponseTemplate();
try {
lasterror = null;
connection = (HttpURLConnection) httpURL.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
BufferedWriter writer =
new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
query.saveXml(writer);
writer.close();
XmlPullParser parser = new NonThreadedXmlPullParserImpl(connection.getInputStream(),
"response", xmlErrorHandler, false);
if (parser.peek().getName().equals("error")) {
ResponseError respError = new ResponseError();
respError.restoreXml(parser, vectorFactory);
parser.dispose();
lasterror =
new FunctionDatabase.BSimError(ErrorCategory.Fatal, respError.errorMessage);
query.clearResponse();
return null;
}
QueryResponseRecord response = query.getResponse();
response.restoreXml(parser, vectorFactory);
parser.dispose();
if (response instanceof ResponseInfo) {
// Query is one of CreateDatabase, InstallCategoryRequest, InstallMetadataRequest, or QueryInfo
info = ((ResponseInfo) response).info;
status = Status.Ready;
isinit = true;
}
return response;
}
catch (Exception ex) {
lasterror = new FunctionDatabase.BSimError(ErrorCategory.Connection, ex.getMessage());
status = Status.Error;
query.clearResponse();
return null;
}
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,8 +15,8 @@
*/
package ghidra.features.bsim.query.description;
import java.io.*;
import java.net.MalformedURLException;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.util.*;
@@ -222,16 +222,10 @@ public class ExecutableRecord implements Comparable<ExecutableRecord> {
protected void setRepository(String repo, String newpath) {
repository = null;
if (repo != null) {
URL ghidraURL;
try {
ghidraURL = new URL(repo);
if (!GhidraURL.isGhidraURL(repo) || (!GhidraURL.isServerRepositoryURL(ghidraURL) &&
!GhidraURL.isLocalProjectURL(ghidraURL))) {
throw new IllegalArgumentException("Unsupported repository URL: " + repo);
}
}
catch (MalformedURLException e) {
throw new IllegalArgumentException("Unsupported repository URL: " + repo, e);
URL ghidraURL = GhidraURL.toURL(repo);
if (!GhidraURL.isServerRepositoryURL(ghidraURL) &&
!GhidraURL.isLocalURL(ghidraURL)) {
throw new IllegalArgumentException("Unsupported repository URL: " + repo);
}
URL projectURL = GhidraURL.getProjectURL(ghidraURL);
repository = projectURL.toExternalForm();
@@ -473,7 +467,7 @@ public class ExecutableRecord implements Comparable<ExecutableRecord> {
}
final StringBuffer buf = new StringBuffer();
buf.append(repository);
if (GhidraURL.isLocalGhidraURL(repository)) {
if (GhidraURL.isLocalURL(repository)) {
if (!repository.endsWith("?")) {
// local URLs add path as a query string
buf.append("?");
@@ -15,10 +15,8 @@
*/
package ghidra.features.bsim.query.ingest;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.io.*;
import java.net.*;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
@@ -221,19 +219,19 @@ public class BSimLaunchable implements GhidraLaunchable {
return new BulkSignatures(serverInfo, connectingUserName);
}
private void setupGhidraURL(String ghidraURLString) throws MalformedURLException {
private void setupGhidraURL(String ghidraURLString) throws IllegalArgumentException {
if (ghidraURLString == null) {
return;
}
if (!GhidraURL.isGhidraURL(ghidraURLString)) {
throw new MalformedURLException("URL is not ghidra protocol: " + ghidraURLString);
throw new IllegalArgumentException("URL is not ghidra protocol: " + ghidraURLString);
}
ghidraURL = new URL(ghidraURLString);
ghidraURL = GhidraURL.toURL(ghidraURLString);
if (!GhidraURL.isServerRepositoryURL(ghidraURL) &&
!GhidraURL.isLocalProjectURL(ghidraURL)) {
throw new MalformedURLException("Invalid repository URL: " + ghidraURLString);
!GhidraURL.isLocalURL(ghidraURL)) {
throw new IllegalArgumentException("Invalid repository URL: " + ghidraURLString);
}
}
@@ -258,8 +256,18 @@ public class BSimLaunchable implements GhidraLaunchable {
throw new IllegalArgumentException(
"Unable to infer ghidra URL from BSim file DB URL");
}
ghidraURLString = "ghidra://" + bsimURL.getHost() + bsimURL.getPath();
setupGhidraURL(ghidraURLString);
// Derive Ghidra URL from BSim DB host and dbName
String path = bsimURL.getPath();
try {
path = URLDecoder.decode(path, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new MalformedURLException(e.getMessage());
}
String dbName = path.substring(path.lastIndexOf('/') + 1);
ghidraURL = GhidraURL.makeURL(bsimURL.getHost(), -1, dbName);
}
}
else if (ghidraURLString != null) {
@@ -476,7 +484,8 @@ public class BSimLaunchable implements GhidraLaunchable {
}
}
private void processSigAndUpdateOptions(String urlstring) throws MalformedURLException {
private void processSigAndUpdateOptions(String urlstring)
throws IllegalArgumentException, MalformedURLException {
String bsimURLOption = optionValueMap.get(BSIM_URL_OPTION);
String configOption = optionValueMap.get(CONFIG_OPTION);
if (configOption != null) {
@@ -53,7 +53,7 @@ public abstract class IterateRepository {
public void process(URL ghidraURL, TaskMonitor monitor) throws Exception, CancelledException {
if (!GhidraURL.isServerRepositoryURL(ghidraURL) &&
!GhidraURL.isLocalProjectURL(ghidraURL)) {
!GhidraURL.isLocalURL(ghidraURL)) {
throw new MalformedURLException("Unsupported repository URL: " + ghidraURL);
}
@@ -20,6 +20,8 @@ import java.io.File;
import javax.swing.*;
import org.apache.commons.lang3.StringUtils;
import docking.ReusableDialogComponentProvider;
import docking.widgets.OptionDialog;
import docking.widgets.button.BrowseButton;
@@ -234,17 +236,23 @@ public class ArchiveDialog extends ReusableDialogComponentProvider {
*/
private boolean checkInput() {
String pathname = getArchivePathName();
if ((pathname == null) || (pathname.equals(""))) {
if (StringUtils.isBlank(pathname)) {
setStatusText("Specify an archive file.");
return false;
}
File file = new File(pathname);
String name = file.getName();
if (!NamingUtilities.isValidProjectName(name)) {
setStatusText("Archive name contains invalid characters.");
// Impose same naming restrictions as Project name uses
try {
NamingUtilities.checkName(name, "Archive name");
}
catch (IllegalArgumentException e) {
setStatusText(e.getMessage());
return false;
}
return true;
}
@@ -322,7 +330,7 @@ public class ArchiveDialog extends ReusableDialogComponentProvider {
jarFile = new File(archivePathName);
}
else if (projectLocator != null) {
jarFile = new File(projectLocator.toString() + ArchivePlugin.ARCHIVE_EXTENSION);
jarFile = new File(projectLocator.getName() + ArchivePlugin.ARCHIVE_EXTENSION);
}
jarFileChooser.setSelectedFile(jarFile);
jarFileChooser.setApproveButtonText(approveButtonText);
@@ -341,15 +349,7 @@ public class ArchiveDialog extends ReusableDialogComponentProvider {
String name = file.getName();
if (!NamingUtilities.isValidProjectName(name)) {
Msg.showError(getClass(), null, "Invalid Archive Name",
name + " is not a valid archive name");
continue;
}
File f = projectLocator.getProjectDir();
String filename = f.getAbsolutePath();
if (chosenPathname.indexOf(filename) >= 0) {
Msg.showError(getClass(), null, "Invalid Archive Name",
"Output file cannot be inside of Project");
"Archive name contains invalid characters or is too long");
continue;
}
@@ -60,7 +60,7 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
private JTextField projectNameField;
private String archivePathName;
private ProjectLocator restoreURL;
private ProjectLocator restoreLocator;
public RestoreDialog(ArchivePlugin plugin) {
super("Restore Project Archive");
@@ -246,14 +246,14 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
/**
* Display this dialog.
* @param pathName The pathname of the archive file containing the data to restore.
* @param projectLocator The project URL of the location to which the restore archive will be
* @param projectLocator The project locator of the location to which the restore archive will be
* extracted.
*
* @return true if the user submitted a valid value, false if user cancelled.
*/
public boolean showDialog(String pathName, ProjectLocator projectLocator) {
this.archivePathName = pathName;
this.restoreURL = projectLocator;
this.restoreLocator = projectLocator;
String projectName = projectNameField.getText();
if (projectName == null || projectName.equals("")) {
projectName = ArchivePlugin.getProjectName(pathName);
@@ -287,11 +287,11 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
}
/**
* Get the URL for the restore directory.
* @return the URL for the restore directory.
* Get the project locator for the restore directory.
* @return the project locator for the restore directory.
*/
ProjectLocator getRestoreURL() {
return restoreURL;
return restoreLocator;
}
/////////////////////////////////////////////
@@ -316,17 +316,24 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
return false;
}
String restoreProjectName = projectNameField.getText().trim();
if (restoreProjectName == null || restoreProjectName.equals("") ||
!NamingUtilities.isValidName(restoreProjectName)) {
if (restoreProjectName == null || restoreProjectName.equals("")) {
setStatusText("Specify a valid project name.");
return false;
}
archivePathName = archiveName;
restoreURL = new ProjectLocator(restoreDir, restoreProjectName);
try {
NamingUtilities.checkProjectName(restoreProjectName);
}
catch (IllegalArgumentException e) {
setStatusText(e.getMessage());
return false;
}
File projFile = restoreURL.getMarkerFile();
File projDir = restoreURL.getProjectDir();
archivePathName = archiveName;
restoreLocator = new ProjectLocator(restoreDir, restoreProjectName);
File projFile = restoreLocator.getMarkerFile();
File projDir = restoreLocator.getProjectDir();
setStatusText("");
if (projFile.exists() || projDir.exists()) {
Msg.showInfo(getClass(), getComponent(), "Project Exists",
@@ -426,12 +433,6 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
}
File file = selectedFile;
String chosenName = file.getName();
if (!NamingUtilities.isValidProjectName(chosenName)) {
Msg.showError(getClass(), null, "Invalid Archive Name",
chosenName + " is not a valid archive name");
continue;
}
Preferences.setProperty(ArchivePlugin.LAST_ARCHIVE_DIR, file.getParent());
pathname = file.getAbsolutePath();
@@ -452,8 +453,8 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
String chooseDirectory(String approveButtonText, String approveToolTip) {
GhidraFileChooser dirChooser = createDirectoryChooser();
dirChooser.setTitle("Restore a Ghidra Project - Directory");
if (restoreURL != null) {
dirChooser.setSelectedFile(new File(restoreURL.getLocation()));
if (restoreLocator != null) {
dirChooser.setSelectedFile(new File(restoreLocator.getLocation()));
}
dirChooser.setApproveButtonText(approveButtonText);
dirChooser.setApproveButtonToolTipText(approveToolTip);
@@ -21,6 +21,7 @@ import java.awt.datatransfer.Clipboard;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -238,16 +239,14 @@ public class ShowInstructionInfoPlugin extends ProgramPlugin {
return null;
}
URL url = new File(filename).toURI().toURL();
URI uri = new File(filename).toURI();
String pageNumber = entry.getPageNumber();
if (pageNumber != null) {
// include manual page as query string (respected by PDF readers)
String fileNameAndPage = url.getFile() + "#page=" + pageNumber;
url = new URL(url.getProtocol(), null, fileNameAndPage);
String pageRef = "#page=" + pageNumber;
uri = uri.resolve(pageRef);
}
return url;
return uri.toURL();
}
ManualEntry locateManualEntry(ProgramActionContext context, Language language) {
@@ -16,8 +16,7 @@
package ghidra.app.plugin.core.progmgr;
import java.beans.PropertyEditor;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@@ -44,6 +43,7 @@ import ghidra.framework.model.*;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
@@ -187,11 +187,10 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager, Opti
@Override
public Program openProgram(URL ghidraURL, int state) {
String location = ghidraURL.getRef();
Program program = openProgram(new ProgramLocator(ghidraURL), state);
String location = GhidraURL.getDecodedReference(ghidraURL);
if (program != null && location != null && state == OPEN_CURRENT) {
gotoProgramRef(program, ghidraURL.getRef());
gotoProgramRef(program, location);
programMgr.saveLocation();
}
return program;
@@ -904,9 +903,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager, Opti
return null;
}
try {
return new URL(url);
return new URI(url).toURL();
}
catch (MalformedURLException e) {
catch (MalformedURLException | URISyntaxException e) {
return null;
}
}
@@ -267,7 +267,7 @@ public class HeadlessAnalyzer {
throw new MalformedURLException("Unsupported repository URL: " + ghidraURL);
}
if (GhidraURL.isLocalProjectURL(ghidraURL)) {
if (GhidraURL.isLocalURL(ghidraURL)) {
Msg.error(this,
"Ghidra URL command form does not supported local project URLs (ghidra:/path...)");
return;
@@ -1808,10 +1808,7 @@ public class HeadlessAnalyzer {
try {
tempProject = new HeadlessProject(getProjectManager(), locator);
}
catch (NotOwnerException e) {
throw new IOException(e);
}
catch (LockException e) {
catch (NotFoundException | NotOwnerException | LockException e) {
throw new IOException(e);
}
@@ -1852,7 +1849,7 @@ public class HeadlessAnalyzer {
private static class HeadlessProject extends DefaultProject {
HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator)
throws NotOwnerException, LockException, IOException {
throws NotFoundException, NotOwnerException, LockException, IOException {
super(projectManager, projectLocator, false);
AppInfo.setActiveProject(this);
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,8 +15,7 @@
*/
package ghidra.app.util.viewer.field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import docking.widgets.fieldpanel.field.AttributedString;
import generic.theme.GThemeDefaults.Colors.Messages;
@@ -78,15 +77,12 @@ public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
}
private URL getURLForString(String urlString) {
URL url = null;
try {
url = new URL(urlString);
return new URI(urlString).toURL();
}
catch (MalformedURLException exc) {
// we return null
catch (MalformedURLException | URISyntaxException e) {
return null;
}
return url;
}
@Override
@@ -18,8 +18,8 @@ package ghidra.framework.main.datatree;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DropTargetDropEvent;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
@@ -64,12 +64,12 @@ public final class LinuxFileUrlHandler extends AbstractFileListFlavorHandler {
return toFiles(transferData, s -> {
try {
return new File(new URL(s.replaceAll(" ", "%20")).toURI()); // fixup spaces
return new File(new URI(s));
}
catch (MalformedURLException e) {
catch (URISyntaxException e) {
// this could be the case that this handler is attempting to process an arbitrary
// String that is not actually a URL
Msg.trace(this, "Not a URL: '" + s + "'", e);
Msg.trace(this, "Not a valid URL: '" + s + "'", e);
return null;
}
catch (Exception e) {
@@ -4,9 +4,9 @@
* 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.
@@ -133,15 +133,23 @@ public class SaveToolConfigDialogTest extends AbstractGhidraHeadedIntegrationTes
@Test
public void testInvalidName() throws Exception {
setText(toolNameField, "My Test Tool", true);
JLabel statusLabel = (JLabel) findComponentByName(saveDialog, "statusLabel");
String msg = statusLabel.getText();
pressButtonByText(saveDialog, "Cancel");
while (saveDialog.isVisible()) {
Thread.sleep(5);
}
setText(toolNameField, "My Test Tool", true);
waitForSwing();
assertEquals("Name cannot have spaces.", msg);
String msg = statusLabel.getText();
assertEquals("Name cannot have spaces", msg);
setText(toolNameField, ".MyTestTool", true);
waitForSwing();
msg = statusLabel.getText();
assertEquals("Name cannot start with a '.'", msg);
setText(toolNameField, "My`Tool`", true);
waitForSwing();
msg = statusLabel.getText();
assertEquals("Invalid character in name: '`'", msg);
}
@Test
@@ -16,7 +16,7 @@
package ghidra;
import java.io.IOException;
import java.net.URL;
import java.net.*;
import java.nio.file.Path;
import docking.framework.DockingApplicationConfiguration;
@@ -59,8 +59,10 @@ public class GhidraGo implements GhidraLaunchable {
if (args != null && args.length > 0) {
ghidra.framework.protocol.ghidra.Handler.registerHandler();
sender = new GhidraGoSender();
// check if ghidra url is valid
GhidraURL.getProjectURL(new URL(args[0]));
URL ghidraUrl = new URI(args[0]).toURL();
GhidraURL.getProjectURL(ghidraUrl); // perform Ghidra URL validation only
startGhidraIfNeeded(layout);
@@ -76,7 +78,7 @@ public class GhidraGo implements GhidraLaunchable {
throw new IllegalArgumentException();
}
}
catch (IllegalArgumentException e) {
catch (URISyntaxException | MalformedURLException | IllegalArgumentException e) {
System.err.println("\n" + "USAGE: ghidraGo <ghidraURL>\n\n" +
"Ghidra URL Forms (ghidraURL):\n" +
" ghidra://<hostname>[:<port>]/<repo-name>[/<folder-path>[/<program-name>]]\n" +
@@ -172,6 +174,6 @@ public class GhidraGo implements GhidraLaunchable {
}
Msg.info(this, "Starting new Ghidra using ghidraRun script at " + ghidraRunPath);
return Runtime.getRuntime().exec(ghidraRunPath.toString());
return Runtime.getRuntime().exec(new String[] { ghidraRunPath.toString() });
}
}
@@ -34,7 +34,7 @@ import ghidra.util.Msg;
packageName = CorePluginPackage.NAME,
shortDescription = "Listens for new GhidraURL's to launch using FrontEndTool's" +
" accept method",
description = "Polls the ghidraGo directory for any url files written by the " +
description = "Polls the ghidraGo directory for any URL files written by the " +
"GhidraGoSender and processes them in Ghidra",
eventsConsumed = {ProjectPluginEvent.class})
//@formatter:on
@@ -4,9 +4,9 @@
* 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.
@@ -130,9 +130,13 @@ public class RepositoryManager {
validateUser(currentUser);
if (!NamingUtilities.isValidProjectName(name)) {
throw new IOException("Invalid repository name: " + name);
try {
NamingUtilities.checkName(name, "Repository name");
}
catch (IllegalArgumentException e) {
throw new IOException(e.getMessage());
}
if (repositoryMap.containsKey(name)) {
throw new DuplicateFileException("Repository named " + name + " already exists");
}
@@ -28,7 +28,6 @@ import org.apache.logging.log4j.Logger;
import generic.hash.HashUtilities;
import ghidra.framework.remote.User;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.NamingUtilities;
import ghidra.util.NumericUtilities;
import ghidra.util.exception.DuplicateNameException;
@@ -758,14 +757,13 @@ public class UserManager {
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9.\\-_/\\\\]*");
/**
* Ensures a name only contains valid characters and meets length limitations.
* Ensures a name only contains valid characters.
*
* @param s name string
* @return boolean true if valid name, false if not valid
*/
public static boolean isValidUserName(String s) {
return VALID_USERNAME_REGEX.matcher(s).matches() &&
s.length() <= NamingUtilities.MAX_NAME_LENGTH;
return VALID_USERNAME_REGEX.matcher(s).matches();
}
}
@@ -110,9 +110,14 @@ public class DefaultClientAuthenticator extends PopupKeyStorePasswordProvider
*/
public static URL getMinimalURL(URL url) {
try {
return new URL(url, "/");
URI uri = url.toURI();
if (uri.isOpaque()) {
// Can't easily simplify - GhidraURL could help but is not visible
return url;
}
return uri.resolve("/").toURL();
}
catch (MalformedURLException e) {
catch (MalformedURLException | URISyntaxException e) {
// ignore
}
return null;
@@ -4,9 +4,9 @@
* 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.
@@ -15,129 +15,135 @@
*/
package ghidra.util;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import ghidra.framework.store.local.LocalFileSystem;
import util.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
/**
* Utility class with static methods for validating project file names.
* {@link NamingUtilities} is a static utility class with methods for validating project file
* names or constrained file path elements.
*/
public final class NamingUtilities {
/**
* Max length for a name.
*/
public final static int MAX_NAME_LENGTH = 60;
private final static char MANGLE_CHAR = '_';
private final static Set<Character> VALID_NAME_SET = CollectionUtils.asSet('.', '-', ' ', '_');
// Restricted character set for Ghidra related file paths.
//
// NOTE: When adding additional characters great care must be taken with Ghidra URI/URL encode/decode
// to ensure that proper roundtrip for path, query and ref/fragment fields work as expected.
// This is particularly a concern with the '+' character.
public final static Set<Character> VALID_NAME_CHARSET =
Collections.unmodifiableSet(
Set.of('.', '-', '=', '@', ' ', '_', '(', ')', '[', ']'));
private NamingUtilities() {
}
/**
* Tests whether the given string is a valid.
* Rules:
* <ul>
* <li>All characters must be a letter, digit (0..9), period, hyphen, underscore or space</li>
* <li>May not exceed a length of 60 characters</li>
* </ul>
* @param name name to validate
* @return true if specified name is valid, else false
* @deprecated method has been deprecated due to improper and widespread use.
* New methods include {@link NamingUtilities#isValidProjectName(String)} and
* {@link LocalFileSystem#testValidName(String,boolean)}.
*/
@Deprecated
public static boolean isValidName(String name) {
if (name == null) {
return false;
}
if ((name.length() < 1) || (name.length() > MAX_NAME_LENGTH)) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!Character.isLetterOrDigit(c) && !VALID_NAME_SET.contains(c)) {
return false;
}
}
return true;
}
/**
* Tests whether the given string is a valid project name.
* <p>
* Rules:
* <ul>
* <li>Name may not be blank (i.e., no characters or all space characters)</li>
* <li>Name may not start with period</li>
* <li>All characters must be a letter, digit (0..9), period, hyphen, underscore or space</li>
* <li>May not exceed a length of 60 characters</li>
* <li>All characters must be a letter, digit (0..9), or within the allowed character set:
* '.', '-', '=', '@', ' ', '_', '(', ')', '[', ']' </li>
* </ul>
*
* @param name name to validate
* @return true if specified name is valid, else false
*/
public static boolean isValidProjectName(String name) {
if (name == null) {
try {
checkProjectName(name);
return true;
}
catch (Exception e) {
return false;
}
if (name.startsWith(".")) {
return false;
}
if ((name.length() < 1) || (name.length() > MAX_NAME_LENGTH)) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!Character.isLetterOrDigit(c) && !VALID_NAME_SET.contains(c)) {
return false;
}
}
return true;
}
/**
* Find the invalid character in the given name.
* <p>
* This method should only be used with {@link #isValidName(String)}} and <b>not</b>
* {@link #isValidProjectName(String)}
* Check the specified project name for character restrictions.
*
* @param name the name with an invalid character
* @return the invalid character or 0 if no invalid character can be found
* @see #isValidName(String)
* @deprecated this method may be removed in a subsequent release due to
* limited use and applicability (project names and project file names have
* different naming restrictions).
* @param name project name
* @throws IllegalArgumentException if name restrictions are violated
*/
@Deprecated
public static char findInvalidChar(String name) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!Character.isLetterOrDigit(c) && !VALID_NAME_SET.contains(c)) {
return c;
}
public static void checkProjectName(String name) throws IllegalArgumentException {
checkName(name, "Project name");
}
/**
* Check the specified project or file path element name for character restrictions.
* The specified path element must exclude any path separators and must nut include any Windows
* drive specification (e.g., {@code C:}). If this naming restriction needs to be imposed on
* an entire path, it must be invoked on each path element separately.
* <p>
* Restrictions include:
* <ul>
* <li>Path element may not be blank (i.e., no characters or all space characters).</li>
* <li>Path element may not start with a '.' which may result in path traversal or hidden
* file/folder use.</li>
* <li>Path element may only contain the letters, numbers, or the following characters:
* '.', '-', '=', '@', ' ', '_', '(', ')', '[', ']'</li>
* </ul>
*
* @param pathElement project or file path element (use of leading and trailing spaces should be
* avoided but is not prohibited).
* @param elementType descriptive name for type of path element or null for default: "Path element"
* @throws IllegalArgumentException if name restrictions are violated
*/
public static void checkName(String pathElement, String elementType)
throws IllegalArgumentException {
String type = StringUtils.isBlank(elementType) ? "Path element" : elementType;
if (StringUtils.isBlank(pathElement)) {
throw new IllegalArgumentException("A blank " + type + " is not allowed");
}
return (char) 0;
if (pathElement.startsWith(".")) { // also prevents '.' and '..' path elements
throw new IllegalArgumentException(type + " starting with '.' is not permitted");
}
String invalidChar = findInvalidChar(pathElement);
if (invalidChar != null) {
throw new IllegalArgumentException(
type + " contains invalid character: '" + invalidChar + "'");
}
}
/**
* Identify an invalid/unsupported character which may be present in the specific name.
* This method applies to project and individual path name elements only.
*
* @param name string to be scanned
* @return an invalid/unsupported character found or null. A string is used to allow for
* rendering of non-ASCII characters.
*/
public static String findInvalidChar(String name) {
AtomicReference<String> invalidChar = new AtomicReference<>();
name.codePoints().forEach(cp -> {
if (Character.isLetterOrDigit(cp)) {
return;
}
// Allow only ASCII symbols from the whitelist
if (cp <= 0x7F && VALID_NAME_CHARSET.contains((char) cp)) {
return;
}
invalidChar.set(new String(Character.toChars(cp)));
});
return invalidChar.get();
}
/**
* Returns a string such that all uppercase characters in the given string are
* replaced by the MANGLE_CHAR followed by the lowercase version of the character.
* MANGLE_CHARs are replaced by 2 MANGLE_CHARs.
*
* This method is to get around the STUPID windows problem where filenames are
* not case sensitive. Under Windows, Foo.exe and foo.exe represent
* the same filename. To fix this we mangle names first such that Foo.exe becomes
* _foo.exe.
* <p>
* This method is to get around case-insensitive filesystems since Ghidra is case-sensitive.
* To fix this we mangle names first such that "Foo.exe" becomes "_foo.exe".
*
* @param name name string to be mangled
* @return mangled name
@@ -199,6 +205,7 @@ public final class NamingUtilities {
/**
* Performs a validity check on a mangled name
*
* @param name mangled name
* @return true if name can be demangled else false
*/
@@ -47,14 +47,18 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
this.useIndexedFileSystem = useIndexedFileSystem;
}
protected File createEmptyProjectDir() {
File tempDir = new File(AbstractGTest.getTestDirectoryPath());
File dir = new File(tempDir, "testproject");
FileUtilities.deleteDir(dir);
dir.mkdir();
return dir;
}
@Before
public void setUp() throws Exception {
File tempDir = new File(AbstractGTest.getTestDirectoryPath());
projectDir = new File(tempDir, "testproject");
FileUtilities.deleteDir(projectDir);
projectDir.mkdir();
projectDir = createEmptyProjectDir();
System.out.println(projectDir.getAbsolutePath());
fs = LocalFileSystem.getLocalFileSystem(projectDir.getAbsolutePath(), useIndexedFileSystem,
@@ -299,6 +303,20 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
}
@Test
public void testCreateTextData() throws Exception {
fs.createFolder("/", "abc");
String data = "This is a test";
LocalTextDataItem textItem =
fs.createTextDataItem("/abc", "fred", "MyID", "Text", data, null, null);
assertEquals(data, textItem.getTextData());
flushFileSystemEvents();
assertEquals(2, events.size());
checkEvent("Item Created", "/abc", "fred", null, null, events.get(1));
}
@Test
public void testFileNames() throws Exception {
@@ -615,17 +633,23 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
DataFileItem df = fs.createDataFile("/abc", "fred", new ByteArrayInputStream(dataBytes),
null, "Data", null);
LocalTextDataItem textItem =
fs.createTextDataItem("/abc", "ted", "MyID", "Text", data, null, null);
assertEquals(data, textItem.getTextData());
createDatabase("/abc", "greg", "123");
String[] items = fs.getItemNames("/abc");
assertEquals(3, items.length);
assertEquals(4, items.length);
assertEquals("bob", items[0]);
assertEquals("fred", items[1]);
assertEquals("greg", items[2]);
assertEquals("ted", items[3]);
assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[0]).getClass());
assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[1]).getClass());
assertEquals(LocalDatabaseItem.class, fs.getItem("/abc", items[2]).getClass());
assertEquals(LocalTextDataItem.class, fs.getItem("/abc", items[3]).getClass());
df = (DataFileItem) fs.getItem("/abc", items[0]);
InputStream is = df.getInputStream();
@@ -642,6 +666,9 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
assertNotNull(dbh.getTable("test"));
dbh.close();
textItem = (LocalTextDataItem) fs.getItem("/abc", items[3]);
assertEquals(data, textItem.getTextData());
}
@Test
@@ -673,6 +700,32 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
checkEvent("Item Moved", "/abc", "fred", "/xyz", "bob", events.get(1));
}
@Test
public void testMoveTextDataFile() throws Exception {
fs.createFolder("/", "abc");
String data = "This is a test";
LocalTextDataItem textItem =
fs.createTextDataItem("/abc", "fred", "MyID", "Text", data, null, null);
assertEquals(data, textItem.getTextData());
flushFileSystemEvents();
events.clear();
FolderItem item = fs.getItem("/abc", "fred");
assertNotNull(item);
fs.moveItem("/abc", "fred", "/xyz", "bob");
assertNull(fs.getItem("/abc", "fred"));
LocalTextDataItem df = (LocalTextDataItem) fs.getItem("/xyz", "bob");
assertNotNull(df);
assertEquals(data, df.getTextData());
flushFileSystemEvents();
assertEquals(2, events.size());
checkEvent("Folder Created", "/", "xyz", null, null, events.get(0));
checkEvent("Item Moved", "/abc", "fred", "/xyz", "bob", events.get(1));
}
@Test
public void testMoveDatabase() throws Exception {
fs.createFolder("/", "abc");
@@ -4,9 +4,9 @@
* 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.
@@ -15,8 +15,7 @@
*/
package ghidra.util;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import resources.ResourceManager;
@@ -151,9 +150,9 @@ public class HelpLocation {
// try creating a URL with the given anchor
if (localURL != null && localAnchor != null) {
try {
localURL = new URL(localURL.toExternalForm() + "#" + localAnchor);
localURL = localURL.toURI().resolve("#" + localAnchor).toURL();
}
catch (MalformedURLException e) {
catch (MalformedURLException | URISyntaxException e) {
// we tried
}
}
@@ -33,8 +33,7 @@ import ghidra.framework.store.local.LocalFileSystem;
import ghidra.framework.store.local.LocalFolderItem;
import ghidra.framework.store.remote.RemoteFileSystem;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.exception.*;
import ghidra.util.task.*;
import utilities.util.FileUtilities;
@@ -105,14 +104,16 @@ public class DefaultProjectData implements ProjectData {
* @param isInWritableProject true if project content is writable, false if project is read-only
* @param resetOwner true to reset the project owner
* @throws IOException if an i/o error occurs
* @throws NotFoundException if project does not exist
* @throws NotOwnerException if inProject is true and user is not owner
* @throws LockException if {@code isInWritableProject} is true and unable to establish project
* write lock (i.e., project in-use)
* @throws FileNotFoundException if project directory not found
*/
public DefaultProjectData(ProjectLocator localStorageLocator, boolean isInWritableProject,
boolean resetOwner) throws NotOwnerException, IOException, LockException {
boolean resetOwner)
throws NotFoundException, NotOwnerException, IOException, LockException {
localStorageLocator.checkProjectExistence();
this.localStorageLocator = localStorageLocator;
boolean success = false;
try {
@@ -158,6 +159,7 @@ public class DefaultProjectData implements ProjectData {
*/
public DefaultProjectData(ProjectLocator localStorageLocator, RepositoryAdapter repository,
boolean isInWritableProject) throws IOException, LockException {
localStorageLocator.checkLocationExistence();
this.localStorageLocator = localStorageLocator;
this.repository = repository;
boolean success = false;
@@ -248,7 +250,7 @@ public class DefaultProjectData implements ProjectData {
}
/**
* Read the contents of the project properties file to include the following values if relavent:
* Read the contents of the project properties file to include the following values if relevant:
* {@value #OWNER}, {@value #SERVER_NAME}, {@value #REPOSITORY_NAME}, {@value #PORT_NUMBER}
* @param projectDir project directory (*.rep)
* @return project properties or null if invalid project directory specified
@@ -297,9 +299,6 @@ public class DefaultProjectData implements ProjectData {
localStorageLocator.getMarkerFile().createNewFile();
}
else {
if (!projectDir.isDirectory()) {
throw new FileNotFoundException("Project directory not found: " + projectDir);
}
if (properties.exists()) {
if (isInWritableProject && properties.isReadOnly()) {
throw new ReadOnlyException(
@@ -17,7 +17,6 @@ package ghidra.framework.data;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
@@ -27,7 +26,8 @@ import ghidra.framework.client.*;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.RepositoryItem;
import ghidra.framework.store.*;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version;
import ghidra.framework.store.db.PackedDatabase;
import ghidra.util.InvalidNameException;
import ghidra.util.ReadOnlyException;
@@ -115,22 +115,6 @@ public class DomainFileProxy implements DomainFile {
return parentPath + DomainFolder.SEPARATOR + getName();
}
private URL getSharedFileURL(URL sharedProjectURL, String ref) {
try {
// Direct URL construction done so that ghidra protocol extension may be supported
String urlStr = sharedProjectURL.toExternalForm();
if (urlStr.endsWith(FileSystem.SEPARATOR)) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
urlStr += getPathname();
return new URL(urlStr);
}
catch (MalformedURLException e) {
// ignore
}
return null;
}
private URL getSharedFileURL(Properties properties, String ref) {
if (properties == null) {
return null;
@@ -183,7 +167,7 @@ public class DomainFileProxy implements DomainFile {
if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) {
URL projectURL = projectLocation.getURL();
if (GhidraURL.isServerRepositoryURL(projectURL)) {
return getSharedFileURL(projectURL, ref);
return GhidraURL.resolve(projectURL, getPathname(), ref);
}
Properties properties =
DefaultProjectData.readProjectProperties(projectLocation.getProjectDir());
@@ -17,7 +17,6 @@ package ghidra.framework.data;
import java.awt.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
@@ -398,16 +397,7 @@ public class GhidraFileData {
RepositoryAdapter repository = projectData.getRepository();
if (versionedFolderItem != null && repository != null) {
URL folderURL = parent.getDomainFolder().getSharedProjectURL();
try {
String spec = name;
if (!StringUtils.isEmpty(ref)) {
spec += "#" + ref;
}
return new URL(folderURL, spec);
}
catch (MalformedURLException e) {
// ignore
}
return GhidraURL.resolve(folderURL, getPathname(), ref);
}
return null;
}
@@ -751,8 +741,15 @@ public class GhidraFileData {
}
// Handle link to Ghidra URL
URL ghidraUrl = new URL(resolvedLinkPath);
doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, false);
try {
URL ghidraUrl = GhidraURL.toURL(resolvedLinkPath);
doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, false);
}
catch (IllegalArgumentException e) {
// Bad URL from link path
throw new IOException(
"Failed to form valid URL from linkPath: " + resolvedLinkPath, e);
}
}
else {
doa = contentHandler.getReadOnlyObject(item, version, true, consumer, monitor);
@@ -819,8 +816,15 @@ public class GhidraFileData {
}
// Handle link to Ghidra URL
URL ghidraUrl = new URL(resolvedLinkPath);
doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, true);
try {
URL ghidraUrl = GhidraURL.toURL(resolvedLinkPath);
doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, true);
}
catch (IllegalArgumentException e) {
// Bad URL from link path
throw new IOException(
"Failed to form valid URL from linkPath: " + resolvedLinkPath, e);
}
}
else {
doa = contentHandler.getImmutableObject(item, consumer, version, -1, monitor);
@@ -939,8 +943,6 @@ public class GhidraFileData {
}
synchronized (fileSystem) {
boolean isLink = isLink();
FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1);
@@ -2372,7 +2374,7 @@ public class GhidraFileData {
return linkPath;
}
private String getAbsolutePath(String path) throws IOException {
private String getAbsolutePath(String path) {
String absPath = path;
if (!path.startsWith(FileSystem.SEPARATOR)) {
absPath = getParent().getPathname();
@@ -16,7 +16,6 @@
package ghidra.framework.data;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
@@ -183,22 +182,11 @@ public class GhidraFolder implements DomainFolder {
if (projectURL == null) {
return null;
}
try {
// Direct URL construction done so that ghidra protocol extension may be supported
String urlStr = projectURL.toExternalForm();
if (urlStr.endsWith(FileSystem.SEPARATOR)) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
String path = getPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
urlStr += path;
return new URL(urlStr);
}
catch (MalformedURLException e) {
return null;
String path = getPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
return GhidraURL.resolve(projectURL, path, null);
}
@Override
@@ -1505,16 +1505,19 @@ class GhidraFolderData {
DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler<?> lh)
throws IOException {
URL url = new URL(ghidraUrl);
if (!GhidraURL.isLocalGhidraURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(url)) {
URL url = GhidraURL.toURL(ghidraUrl);
if (!GhidraURL.isLocalURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(url)) {
throw new IllegalArgumentException("Invalid Ghidra URL specified");
}
if (url.getRef() != null) {
throw new IllegalArgumentException("URL must not include #reference");
}
// Force use of unique link-file name
String newName = getUniqueName(linkFilename);
try {
lh.createLink(ghidraUrl, fileSystem, getPathname(), newName);
lh.createLink(url.toExternalForm(), fileSystem, getPathname(), newName);
}
catch (InvalidNameException e) {
throw new IOException(e); // unexpected
@@ -1567,6 +1570,9 @@ class GhidraFolderData {
Path relativePath = linkParentPath.relativize(referencedPath);
String path = relativePath.toString();
// When running on Windows path separator may get switched on us. Be sure to switch back.
path = path.replace('\\', '/');
// Re-apply preserved finalRefElement to relative path
if (finalRefElement != null) {
if (!path.isBlank()) {
@@ -16,7 +16,6 @@
package ghidra.framework.data;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@@ -240,7 +239,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
static boolean canShareLink(FolderItem linkFile) {
try {
String linkPath = getLinkPath(linkFile);
return !GhidraURL.isLocalGhidraURL(linkPath);
return !GhidraURL.isLocalURL(linkPath);
}
catch (IOException e) {
// ignore
@@ -304,11 +303,11 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> implements Co
ProjectData projectData = linkFile.getParent().getProjectData();
return GhidraURL.makeURL(projectData.getProjectLocator(), linkPath, null);
}
return new URL(linkPath);
return GhidraURL.toURL(linkPath);
}
catch (MalformedURLException | IllegalArgumentException e) {
catch (IllegalArgumentException e) {
// Bad URL from link path
throw new IOException("Failed to form URL from linkPath: " + linkPath, e);
throw new IOException("Failed to form valid URL from linkPath: " + linkPath, e);
}
}
@@ -17,7 +17,6 @@ package ghidra.framework.data;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
@@ -25,8 +24,6 @@ import java.util.Map;
import javax.help.UnsupportedOperationException;
import javax.swing.Icon;
import org.apache.commons.lang3.StringUtils;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.*;
@@ -125,16 +122,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
public URL getSharedProjectURL(String ref) {
URL folderURL = parent.getSharedProjectURL();
if (GhidraURL.isServerRepositoryURL(folderURL)) {
try {
String spec = fileName;
if (!StringUtils.isEmpty(ref)) {
spec += "#" + ref;
}
return new URL(folderURL, spec);
}
catch (MalformedURLException e) {
// ignore
}
return GhidraURL.resolve(folderURL, getLinkedPathname(), ref);
}
return null;
}
@@ -65,7 +65,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
super(folderLinkFile.getName());
if (!GhidraURL.isServerRepositoryURL(linkedFolderUrl) &&
!GhidraURL.isLocalProjectURL(linkedFolderUrl)) {
!GhidraURL.isLocalURL(linkedFolderUrl)) {
throw new IllegalArgumentException("Invalid Ghidra URL: " + linkedFolderUrl);
}
@@ -16,7 +16,6 @@
package ghidra.framework.data;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.Icon;
@@ -167,20 +166,11 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
public URL getSharedProjectURL() {
URL projectURL = getLinkedRootFolder().getProjectURL();
if (GhidraURL.isServerRepositoryURL(projectURL)) {
String urlStr = projectURL.toExternalForm();
if (urlStr.endsWith(FileSystem.SEPARATOR)) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
String path = getLinkedPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
try {
return new URL(urlStr + path);
}
catch (MalformedURLException e) {
// ignore
}
return GhidraURL.resolve(projectURL, path, null);
}
return null;
}
@@ -517,25 +517,29 @@ public class FrontEndPlugin extends Plugin
// the extension, try to open or create using the extension
else if (!create && filename.lastIndexOf(".") > path.lastIndexOf(File.separator)) {
// treat opening a file without the ghidra extension as an error
Msg.showError(getClass(), tool.getToolFrame(), "Invalid Project File",
Msg.showError(this, tool.getToolFrame(), "Invalid Project File",
"Cannot open '" + file.getName() + "' as a Ghidra Project");
continue;
}
if (!NamingUtilities.isValidProjectName(filename)) {
Msg.showError(getClass(), tool.getToolFrame(), "Invalid Project Name",
filename + " is not a valid project name");
continue;
}
Preferences.setProperty(preferenceName, path);
try {
ProjectLocator projectLocator = new ProjectLocator(path, filename);
Preferences.setProperty(preferenceName, path);
Preferences.store();
return projectLocator;
}
catch (IllegalArgumentException e) {
Msg.showError(this, tool.getToolFrame(), "Invalid Project Name",
e.getMessage());
continue;
}
catch (Exception e) {
Msg.debug(this,
"Unexpected exception storing preferences to" + Preferences.getFilename(),
e);
}
return new ProjectLocator(path, filename);
}
return null;
}
@@ -181,7 +181,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
@Override
public boolean accept(URL url) {
if (!GhidraURL.isLocalProjectURL(url) && !GhidraURL.isServerRepositoryURL(url)) {
if (!GhidraURL.isLocalURL(url) && !GhidraURL.isServerRepositoryURL(url)) {
return false;
}
Swing.runLater(() -> execute(new AcceptUrlContentTask(url, true, plugin)));
@@ -16,7 +16,6 @@
package ghidra.framework.main;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
@@ -35,7 +34,8 @@ import ghidra.framework.model.*;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User;
import ghidra.util.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
class ProjectActionManager {
private final static String CLOSE_ALL_OPEN_VIEWS = "Close All Read-Only Views";
@@ -536,11 +536,11 @@ class ProjectActionManager {
String urlStr = Preferences.getProperty(LAST_VIEWED_REPOSITORY_URL);
URL lastURL = null;
if (urlStr != null) {
if (GhidraURL.isGhidraURL(urlStr)) {
try {
lastURL = new URL(urlStr);
lastURL = GhidraURL.toURL(urlStr);
}
catch (MalformedURLException e) {
catch (IllegalArgumentException e) {
// ignore
}
}
@@ -16,8 +16,6 @@
package ghidra.framework.main;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import docking.action.MenuData;
@@ -73,10 +71,10 @@ public class ProjectDataFollowLinkAction extends FrontendProjectTreeAction {
if (GhidraURL.isGhidraURL(linkPath)) {
// Follow URL using a project view
try {
plugin.showInViewedProject(new URL(linkPath), isFolderLink);
plugin.showInViewedProject(GhidraURL.toURL(linkPath), isFolderLink);
return;
}
catch (MalformedURLException e) {
catch (IllegalArgumentException e) {
Msg.error(this, "Invalid link URL: " + e.getMessage());
return;
}
@@ -20,7 +20,6 @@ import java.awt.CardLayout;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.*;
@@ -255,15 +254,16 @@ class RepositoryChooser extends ReusableDialogComponentProvider {
setOkEnabled(false);
try {
URL url = new URL(urlTextField.getText());
if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) {
String urlText = urlTextField.getText();
if (!GhidraURL.isGhidraURL(urlText)) {
setStatusText("URL must specify 'ghidra:' protocol", MessageType.ERROR);
}
else {
GhidraURL.toURL(urlText); // check ability to form URL instance
setOkEnabled(true);
}
}
catch (MalformedURLException e) {
catch (IllegalArgumentException e) {
setStatusText(e.getMessage(), MessageType.ERROR);
}
@@ -294,9 +294,9 @@ class RepositoryChooser extends ReusableDialogComponentProvider {
// TODO: How do we restrict URL to repository only - not sure we can
try {
return new URL(urlTextField.getText());
return GhidraURL.toURL(urlTextField.getText());
}
catch (MalformedURLException e) {
catch (IllegalArgumentException e) {
Msg.error(this, e.getMessage());
}
return null;
@@ -16,7 +16,6 @@
package ghidra.framework.main.datatree;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
@@ -324,8 +323,8 @@ public class DomainFileNode extends DataTreeNode {
if (GhidraURL.isGhidraURL(linkPath)) {
try {
URL url = new URL(linkPath);
if (GhidraURL.isLocalGhidraURL(linkPath)) {
URL url = GhidraURL.toURL(linkPath);
if (GhidraURL.isLocalURL(linkPath)) {
ProjectLocator loc = GhidraURL.getProjectStorageLocator(url);
if (loc != null) {
String projectPath = GhidraURL.getProjectPathname(url);
@@ -341,7 +340,7 @@ public class DomainFileNode extends DataTreeNode {
}
}
}
catch (MalformedURLException e) {
catch (IllegalArgumentException e) {
// ignore - use original linkPath
}
}
@@ -20,6 +20,8 @@ import java.util.List;
import javax.swing.JComponent;
import org.apache.commons.lang3.StringUtils;
import docking.wizard.WizardModel;
import docking.wizard.WizardStep;
import ghidra.app.util.GenericHelpTopics;
@@ -77,10 +79,20 @@ public class RepositoryStep extends WizardStep<ProjectWizardData> {
if (repositoryName.length() == 0) {
return false;
}
if (!NamingUtilities.isValidProjectName(repositoryName)) {
setStatusMessage("Invalid project repository name");
if (StringUtils.isBlank(repositoryName)) {
setStatusMessage("Enter project repository name");
return false;
}
try {
NamingUtilities.checkName(repositoryName,
"Repository name");
}
catch (IllegalArgumentException e) {
setStatusMessage(e.getMessage());
return false;
}
if (List.of(repositoryNames).contains(repositoryName)) {
setStatusMessage("Repository " + repositoryName + " already exists");
return false;
@@ -21,11 +21,12 @@ import java.io.File;
import javax.swing.JComponent;
import org.apache.commons.lang3.StringUtils;
import docking.wizard.WizardModel;
import docking.wizard.WizardStep;
import ghidra.framework.model.ProjectLocator;
import ghidra.util.HelpLocation;
import ghidra.util.NamingUtilities;
/**
* Wizard step in the new project wizard for choosing the new project's root folder location and
@@ -76,7 +77,7 @@ public class SelectProjectStep extends WizardStep<ProjectWizardData> {
return false;
}
if (!NamingUtilities.isValidProjectName(projectName)) {
if (StringUtils.isBlank(projectName)) {
setStatusMessage("Please specify valid project name");
return false;
}
@@ -16,13 +16,16 @@
package ghidra.framework.model;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import ghidra.framework.Application;
import ghidra.framework.OperatingSystem;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.NamingUtilities;
import ghidra.util.exception.NotFoundException;
/**
* Lightweight descriptor of a local Project storage location.
@@ -36,20 +39,15 @@ public class ProjectLocator {
private final String name;
private final String location;
private final boolean isWindowsOnlyLocation; // drive letter or UNC path specified
private URL url;
/**
* Set of characters specifically disallowed in project name or path.
* These characters may interfere with path and URL parsing.
*/
public static Set<Character> DISALLOWED_CHARS = Set.of(':', ';', '&', '?', '#');
/**
* Construct a project locator object.
* @param path absolute path to parent directory (may or may not exist). The user's temp directory
* will be used if this value is null or blank. The use of "\" characters will always be replaced
* with "/".
* with "/". A path starting with either "//" or "\\" will be treated as a Windows UNC path.
* WARNING: Use of a relative paths should be avoided (e.g., on a windows platform
* an absolute path should start with a drive letter specification such as C:\path).
* A path such as "/path" on windows will utilize the current default drive and will
@@ -58,15 +56,15 @@ public class ProjectLocator {
* @param name name of the project (may only contain alphanumeric characters or
* @throws IllegalArgumentException if an absolute path is not specified or invalid project name
*/
public ProjectLocator(String path, String name) {
public ProjectLocator(String path, String name) throws IllegalArgumentException {
this(path, name, null);
}
protected ProjectLocator(String path, String name, URL url) {
protected ProjectLocator(String path, String name, URL url) throws IllegalArgumentException {
if (name.contains("/") || name.contains("\\")) {
throw new IllegalArgumentException("name contains path separator character: " + name);
}
checkInvalidChar("name", name, 0);
NamingUtilities.checkProjectName(name);
if (name.endsWith(PROJECT_FILE_SUFFIX)) {
name = name.substring(0, name.length() - PROJECT_FILE_SUFFIX.length());
}
@@ -74,78 +72,11 @@ public class ProjectLocator {
if (StringUtils.isBlank(path)) {
path = Application.getUserTempDirectory().getAbsolutePath();
}
this.location = checkAbsolutePath(path);
location = GhidraURL.checkLocalAbsolutePath(path, true);
isWindowsOnlyLocation = GhidraURL.isWindowsOnlyPath(location);
this.url = url != null ? url : GhidraURL.makeURL(location, name);
}
/**
* Check for characters explicitly disallowed in path or project name.
* @param type type of string to include in exception
* @param str string to check
* @param startIndex index at which to start checking
* @throws IllegalArgumentException if str contains invalid character
*/
private static void checkInvalidChar(String type, String str, int startIndex) {
for (int i = startIndex; i < str.length(); i++) {
char c = str.charAt(i);
if (DISALLOWED_CHARS.contains(c)) {
throw new IllegalArgumentException(
type + " contains invalid character: '" + c + "'");
}
}
}
/**
* Ensure that absolute path is specified and normalize its format.
* An absolute path may start with a windows drive letter (e.g., c:/a/b, /c:/a/b)
* or without (e.g., /a/b). Although for Windows the lack of a drive letter is
* not absolute, for consistency with Linux we permit this form which on
* Windows will use the default drive for the process. The resulting path
* may be transormed to always end with a "/" and if started with a drive letter
* (e.g., "c:/") it will have a "/" prepended (e.g., "/c:/", both forms
* are treated the same by the {@link File} class under Windows).
* @param path path to be checked and possibly modified.
* @return path to be used
* @throws IllegalArgumentException if an invalid path is specified
*/
private static String checkAbsolutePath(String path) {
int scanIndex = 0;
path = path.replace('\\', '/');
int len = path.length();
if (!path.startsWith("/")) {
// Allow paths to start with windows drive letter (e.g., c:/a/b)
if (len >= 3 && hasAbsoluteDriveLetter(path, 0)) {
path = "/" + path;
}
else {
throw new IllegalArgumentException("absolute path required");
}
scanIndex = 3;
}
else if (len >= 3 && hasDriveLetter(path, 1)) {
if (len < 4 || path.charAt(3) != '/') {
// path such as "/c:" not permitted
throw new IllegalArgumentException("absolute path required");
}
scanIndex = 4;
}
checkInvalidChar("path", path, scanIndex);
if (!path.endsWith("/")) {
path += "/";
}
return path;
}
private static boolean hasDriveLetter(String path, int index) {
return Character.isLetter(path.charAt(index++)) && path.charAt(index) == ':';
}
private static boolean hasAbsoluteDriveLetter(String path, int index) {
int pathIndex = index + 2;
return path.length() > pathIndex && hasDriveLetter(path, index) &&
path.charAt(pathIndex) == '/';
}
/**
* {@return true if this project URL corresponds to a transient project
* (e.g., corresponds to remote Ghidra URL)}
@@ -155,8 +86,9 @@ public class ProjectLocator {
}
/**
* {@return the URL associated with this local project. If using a temporary transient
* project location this URL should not be used.}
* {@return the URL associated with this local project.}
* If using a temporary transient project location this URL will refer to the
* remote server repository.
*/
public URL getURL() {
return url;
@@ -170,9 +102,18 @@ public class ProjectLocator {
}
/**
* Get the location of the project which will contain marker file
* ({@link #getMarkerFile()}) and project directory ({@link #getProjectDir()}).
* Get the absolute path of the directory which contains the project marker file
* ({@link #getMarkerFile()}) and project directory ({@link #getProjectDir()})
* (i.e., parent directory).
* <p>
* Directory path will always use '/' as a path separator and may have one of the following
* forms:
* <ul>
* <li>Standard path: /a/b/</li>
* <li>Windows path: /C:/a/b</li>
* <li>Windows UNC path: //server/share/a/b</li>
* </ul>
* <p>
* Note: directory may or may not exist.
* @return project location directory
*/
@@ -181,24 +122,45 @@ public class ProjectLocator {
}
/**
* {@return the project directory}
* {@return true if this project location is only valid on a Windows platform.}
*/
public File getProjectDir() {
return new File(location, name + PROJECT_DIR_SUFFIX);
public boolean isWindowsOnlyLocation() {
return isWindowsOnlyLocation;
}
private File getParentDir() {
return new File(location);
}
/**
* {@return the file that indicates a Ghidra project.}
* Get the project storage directory associated with this project locator.
* <p>
* NOTE: {@link #exists()} or {@link #checkProjectExistence()} method should be used prior to the
* returned file.
*
* @return the project directory
*/
public File getProjectDir() {
return new File(getParentDir(), name + PROJECT_DIR_SUFFIX);
}
/**
* Get the project marker file associated with this project locator.
* <p>
* NOTE: {@link #exists()} or {@link #checkProjectExistence()} method should be used prior to the
* returned file.
*
* @return the marker file that indicates a Ghidra project.
*/
public File getMarkerFile() {
return new File(location, name + PROJECT_FILE_SUFFIX);
return new File(getParentDir(), name + PROJECT_FILE_SUFFIX);
}
/**
* {@return project lock file to prevent multiple accesses to the same project at once.}
*/
public File getProjectLockFile() {
return new File(location, name + LOCK_FILE_SUFFIX);
return new File(getParentDir(), name + LOCK_FILE_SUFFIX);
}
/**
@@ -249,10 +211,72 @@ public class ProjectLocator {
}
/**
* {@return true if project storage exists}
* Determine if the project exists.
* <p>
* IMPORTANT: This method or {@link #checkProjectExistence()} method should always be used prior to
* using File object returned by {@link #getParentDir()}, {@link #getMarkerFile()} or
* {@link #getProjectLockFile()} since OS-specific checks are performed which may not
* be considered when using the returned File objects.
*
* @return true if project storage exists.
*/
public boolean exists() {
if (isWindowsOnlyLocation &&
OperatingSystem.CURRENT_OPERATING_SYSTEM != OperatingSystem.WINDOWS) {
// Do not try to evaluate a Windows-only path on other platforms
return false;
}
return getMarkerFile().isFile() && getProjectDir().isDirectory();
}
/**
* Verify that this project exists with its required marker file and data storage directory.
* <p>
* IMPORTANT: This method or {@link #exists()} method should always be used prior to
* using File object returned by {@link #getParentDir()}, {@link #getMarkerFile()} or
* {@link #getProjectLockFile()} since OS-specific checks are performed which may not
* be considered when using the returned File objects.
*
* @throws NotFoundException if project does not exist
*/
public void checkProjectExistence() throws NotFoundException {
if (isWindowsOnlyLocation &&
OperatingSystem.CURRENT_OPERATING_SYSTEM != OperatingSystem.WINDOWS) {
throw new NotFoundException(
"The project location is only valid on Windows: " + location);
}
File markerFile = getMarkerFile();
if (!markerFile.isFile()) {
throw new NotFoundException("Project marker file not found: " + markerFile);
}
File projectDir = getProjectDir();
if (!projectDir.isDirectory()) {
throw new NotFoundException("Project directory not found: " + projectDir);
}
}
/**
* Verify that the specified project location directory exists in preparation for creating
* a new project.
* <p>
* IMPORTANT: This method or {@link #exists()} method should always be used prior to
* using File object returned by {@link #getParentDir()}, {@link #getMarkerFile()} or
* {@link #getProjectLockFile()} since OS-specific checks are performed which may not
* be considered when using the returned File objects.
*
* @throws IOException if project location does not exist
*/
public void checkLocationExistence() throws IOException {
if (isWindowsOnlyLocation &&
OperatingSystem.CURRENT_OPERATING_SYSTEM != OperatingSystem.WINDOWS) {
throw new IOException("The project location is only valid on Windows: " + location);
}
File dir = getParentDir();
if (!dir.isDirectory()) {
throw new IOException("Project location not found: " + dir);
}
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -207,13 +207,20 @@ public class SaveToolConfigDialog extends DialogComponentProvider implements Lis
return;
}
// Impose same naming restrictions as Project uses - other than the blank space
if (newName.indexOf(" ") >= 0) {
setStatusText("Name cannot have spaces.");
setStatusText("Name cannot have spaces");
nameField.requestFocus();
return;
}
if (!NamingUtilities.isValidName(newName)) {
setStatusText("Invalid character in name: " + NamingUtilities.findInvalidChar(newName));
if (newName.startsWith(".")) {
setStatusText("Name cannot start with a '.'");
nameField.requestFocus();
return;
}
String invalidChar = NamingUtilities.findInvalidChar(newName);
if (invalidChar != null) {
setStatusText("Invalid character in name: '" + invalidChar + "'");
nameField.requestFocus();
return;
}
@@ -40,6 +40,7 @@ import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.NotFoundException;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
@@ -103,11 +104,13 @@ public class DefaultProject implements Project {
* @param resetOwner if true, set the owner to the current user
* @throws FileNotFoundException project directory not found
* @throws IOException if I/O error occurs.
* @throws NotFoundException if project does not exist
* @throws NotOwnerException if userName is not the owner of the project.
* @throws LockException if unable to establish project lock
*/
protected DefaultProject(DefaultProjectManager projectManager, ProjectLocator projectLocator,
boolean resetOwner) throws IOException, NotOwnerException, LockException {
boolean resetOwner)
throws IOException, NotFoundException, NotOwnerException, LockException {
this.projectManager = projectManager;
this.projectLocator = projectLocator;
@@ -485,7 +488,7 @@ public class DefaultProject implements Project {
while (it.hasNext()) {
Element elem = (Element) it.next();
String urlStr = elem.getAttributeValue("URL");
URL url = new URL(urlStr);
URL url = GhidraURL.toURL(urlStr);
try {
addProjectView(url, true);
}
@@ -127,18 +127,6 @@ public class DefaultProjectManager implements ProjectManager {
throw new LockException(msg);
}
if (!projectLocator.getMarkerFile().exists()) {
forgetProject(projectLocator);
throw new NotFoundException(
"Project marker file not found: " + projectLocator.getMarkerFile());
}
if (!projectLocator.getProjectDir().isDirectory()) {
forgetProject(projectLocator);
throw new NotFoundException(
"Project directory not found: " + projectLocator.getProjectDir());
}
try {
currentProject = new DefaultProject(this, projectLocator, resetOwner);
AppInfo.setActiveProject(currentProject);
@@ -546,7 +534,7 @@ public class DefaultProjectManager implements ProjectManager {
}
}
catch (IllegalArgumentException e) {
Msg.error(this, "Invalid project path: " + path);
Msg.error(this, "Invalid project path: " + path, e);
}
return null;
}
@@ -562,7 +550,7 @@ public class DefaultProjectManager implements ProjectManager {
String urlStr = (String) st.nextElement();
try {
URL url = GhidraURL.toURL(urlStr);
if (GhidraURL.isLocalProjectURL(url) && !GhidraURL.localProjectExists(url)) {
if (GhidraURL.isLocalURL(url) && !GhidraURL.localProjectExists(url)) {
continue;
}
list.add(url);
@@ -255,7 +255,7 @@ class ToolServicesImpl implements ToolServices {
@Override
public PluginTool launchToolWithURL(String toolName, URL ghidraUrl)
throws IllegalArgumentException {
if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
if (!GhidraURL.isLocalURL(ghidraUrl) &&
!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("unsupported URL");
}
@@ -27,6 +27,7 @@ import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.store.LockException;
import ghidra.util.NotOwnerException;
import ghidra.util.ReadOnlyException;
import ghidra.util.exception.NotFoundException;
/**
* <code>DefaultLocalGhidraProtocolConnector</code> provides support for the
@@ -134,6 +135,9 @@ public class DefaultLocalGhidraProtocolConnector extends GhidraProtocolConnector
try {
return new DefaultProjectData(localStorageLocator, !readOnlyAccess, false);
}
catch (NotFoundException e) {
statusCode = StatusCode.NOT_FOUND;
}
catch (NotOwnerException | ReadOnlyException e) {
statusCode = StatusCode.UNAUTHORIZED;
}
@@ -4,9 +4,9 @@
* 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.
@@ -16,8 +16,7 @@
package ghidra.framework.protocol.ghidra;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import org.apache.commons.lang3.StringUtils;
@@ -34,6 +33,7 @@ import ghidra.framework.store.FileSystem;
public abstract class GhidraProtocolConnector {
protected final URL url;
protected final URI uri;
protected String repositoryName; // only valid for server repository
protected final String itemPath; // trailing "/" signifies explicit folder path
@@ -53,6 +53,12 @@ public abstract class GhidraProtocolConnector {
*/
protected GhidraProtocolConnector(URL url) throws MalformedURLException {
this.url = url;
try {
this.uri = url.toURI();
}
catch (URISyntaxException e) {
throw new MalformedURLException(e.getMessage());
}
checkProtocol();
checkUserInfo();
checkHostInfo();
@@ -63,7 +69,7 @@ public abstract class GhidraProtocolConnector {
/**
* Get the URL associated with the repository/project root folder.
* This will be used as a key to its corresponding transient project data.
* @return root folder URL
* @return root folder URL or null if connection is a server-only URL
*/
protected abstract URL getRepositoryRootGhidraURL();
@@ -107,7 +113,7 @@ public abstract class GhidraProtocolConnector {
*/
private String parseRepositoryName() throws MalformedURLException {
String path = url.getPath();
String path = uri.getPath();
// Divide path into pieces
if (StringUtils.isBlank(path) || path.length() < 2 || path.charAt(0) != '/') {
@@ -182,7 +188,7 @@ public abstract class GhidraProtocolConnector {
*/
protected String parseItemPath() throws MalformedURLException {
String path = url.getPath();
String path = uri.getPath();
if (repositoryName == null) {
if (!StringUtils.isEmpty(path) && !"/".equals(path)) {
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More