diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java index f1d7f4b17a..f5442fc1e5 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java @@ -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; } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/BSimSearchResultsProvider.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/BSimSearchResultsProvider.java index 556f09569a..f682719d5e 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/BSimSearchResultsProvider.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/BSimSearchResultsProvider.java @@ -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; } } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/apply/AbstractBSimApplyTask.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/apply/AbstractBSimApplyTask.java index 8ee9624743..f210dd5eb3 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/apply/AbstractBSimApplyTask.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/apply/AbstractBSimApplyTask.java @@ -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; diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimClientFactory.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimClientFactory.java index 291e47316c..a029d2864d 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimClientFactory.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimClientFactory.java @@ -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 } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java index 6960e62cee..3cb1182f16 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java @@ -65,7 +65,7 @@ public class BSimServerInfo implements Comparable { * @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 { * * @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. diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java deleted file mode 100755 index 39ba6e2869..0000000000 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java +++ /dev/null @@ -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; - } - } - -} diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/description/ExecutableRecord.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/description/ExecutableRecord.java index 1860372c62..5a920bd31e 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/description/ExecutableRecord.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/description/ExecutableRecord.java @@ -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 { 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 { } 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("?"); diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java index 1ed9d2ed09..9a5819afc9 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java @@ -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) { diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java index 0e1e11aa06..0ea570071f 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java @@ -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); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/ArchiveDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/ArchiveDialog.java index 0f9b3d42f9..728bc93b3e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/ArchiveDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/ArchiveDialog.java @@ -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; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreDialog.java index 60397a0659..96d0f07623 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/archive/RestoreDialog.java @@ -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); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/ShowInstructionInfoPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/ShowInstructionInfoPlugin.java index 9bcf2649c0..95f69b263b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/ShowInstructionInfoPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/ShowInstructionInfoPlugin.java @@ -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) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java index e309328f8c..718d446349 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java @@ -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; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index 0006065893..a442503e5a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -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); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java index 553d57a88b..704e343b8f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java @@ -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 diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java index bd398d87e6..958fb179a1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java @@ -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) { diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java index a528b49a82..d36e05fb87 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialogTest.java @@ -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 diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java index 123625f230..66d83a8106 100644 --- a/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java @@ -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 \n\n" + "Ghidra URL Forms (ghidraURL):\n" + " ghidra://[:]/[/[/]]\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() }); } } diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java index da8a6f685d..600cc752b8 100644 --- a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java @@ -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 diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java index a2fd22595a..a2759fa11b 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java @@ -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"); } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/UserManager.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/UserManager.java index 861d3a1c5b..515434c47a 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/UserManager.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/UserManager.java @@ -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(); } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java index 413a513b05..cb75aaa057 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java @@ -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; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/NamingUtilities.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/NamingUtilities.java index 3aa7fa9f29..d14538af13 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/NamingUtilities.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/NamingUtilities.java @@ -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 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 VALID_NAME_CHARSET = + Collections.unmodifiableSet( + Set.of('.', '-', '=', '@', ' ', '_', '(', ')', '[', ']')); private NamingUtilities() { } - /** - * Tests whether the given string is a valid. - * Rules: - *
    - *
  • All characters must be a letter, digit (0..9), period, hyphen, underscore or space
  • - *
  • May not exceed a length of 60 characters
  • - *
- * @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. + *

* Rules: *

    + *
  • Name may not be blank (i.e., no characters or all space characters)
  • *
  • Name may not start with period
  • - *
  • All characters must be a letter, digit (0..9), period, hyphen, underscore or space
  • - *
  • May not exceed a length of 60 characters
  • + *
  • All characters must be a letter, digit (0..9), or within the allowed character set: + * '.', '-', '=', '@', ' ', '_', '(', ')', '[', ']'
  • *
+ * * @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. - *

- * This method should only be used with {@link #isValidName(String)}} and not - * {@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. + *

+ * Restrictions include: + *

    + *
  • Path element may not be blank (i.e., no characters or all space characters).
  • + *
  • Path element may not start with a '.' which may result in path traversal or hidden + * file/folder use.
  • + *
  • Path element may only contain the letters, numbers, or the following characters: + * '.', '-', '=', '@', ' ', '_', '(', ')', '[', ']'
  • + *
+ * + * @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 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. + *

+ * 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 */ diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java index 5ca2f5429e..d286a457c9 100644 --- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java +++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java @@ -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"); diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/HelpLocation.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/HelpLocation.java index 867bf7451a..8aa5c7c152 100644 --- a/Ghidra/Framework/Gui/src/main/java/ghidra/util/HelpLocation.java +++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/HelpLocation.java @@ -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 } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java index 9be18a319a..b0bd424482 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java @@ -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( diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index d889af89be..2205485e9e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -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()); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java index da89ff578e..337c68d979 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java @@ -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(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java index 4d1cbc3e22..6eb7534bda 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java @@ -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 diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java index 6295aaf801..688dd155ef 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java @@ -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()) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java index 2ed40febef..75496384b3 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java @@ -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 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 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); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java index d7b2511231..bc3a54ba94 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java @@ -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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java index 955d49b11a..580595c6d9 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java @@ -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); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java index d2274b35ac..4e30e410f5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java @@ -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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java index de4ae05c1d..d50d747127 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java @@ -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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java index 71a2ee92f6..5fb3f23df5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java @@ -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))); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java index 5394486678..26f2192115 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java @@ -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 } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java index 0690854008..d1af4c8117 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java @@ -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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java index c3e7c2f6df..09697895da 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java @@ -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; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java index 0b1f285cd4..e8aab8b3ec 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java @@ -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 } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/RepositoryStep.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/RepositoryStep.java index 0de34ffb62..e0cdd731e8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/RepositoryStep.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/RepositoryStep.java @@ -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 { 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; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/SelectProjectStep.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/SelectProjectStep.java index 6f43247be4..fc9b4f8f9e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/SelectProjectStep.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/wizard/project/SelectProjectStep.java @@ -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 { return false; } - if (!NamingUtilities.isValidProjectName(projectName)) { + if (StringUtils.isBlank(projectName)) { setStatusMessage("Please specify valid project name"); return false; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java index 26668731c1..f8d3e3c8c1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java @@ -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 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). *

+ * Directory path will always use '/' as a path separator and may have one of the following + * forms: + *

    + *
  • Standard path: /a/b/
  • + *
  • Windows path: /C:/a/b
  • + *
  • Windows UNC path: //server/share/a/b
  • + *
+ *

* 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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); + } + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java index 87d27640da..5f4c8ffc1a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/SaveToolConfigDialog.java @@ -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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index 391e981780..4bd3e21f19 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -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); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java index 655463045f..5a8071f01a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java @@ -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); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java index 8826790811..5eabba81d0 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java @@ -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"); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java index f057a10a47..0657a5ef57 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java @@ -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; /** * DefaultLocalGhidraProtocolConnector 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; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java index f21d006560..cfacf0029b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java @@ -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)) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java index c90c8be0f1..90f2eb785f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java @@ -16,35 +16,82 @@ package ghidra.framework.protocol.ghidra; import java.io.File; +import java.io.UnsupportedEncodingException; import java.net.*; import java.util.Objects; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; -import ghidra.framework.model.ProjectLocator; +import ghidra.framework.client.RepositoryServerAdapter; +import ghidra.framework.model.*; import ghidra.framework.remote.GhidraServerHandle; +import ghidra.util.NamingUtilities; /** - * Supported URL forms include: + * Utility class which provides support for creating Ghidra local project and remote repository + * URLs. Valid Ghidra URL forms include: *

    - *
  • {@literal ghidra://:/[/]/[[#ref]]}
  • + *
  • {@literal ghidra:[ext:]//:/[/]/[[#ref]]}
  • *
  • {@literal ghidra:/[X:/]/[?[/]/[[#ref]]]}
  • + *
  • {@literal ghidra:////UNCServer/UNCshare/[?[/]/[[#ref]]]}
  • *
+ *

+ * NOTE: [ext:] corresponds to an optional Ghidra server extension protocol if supported. + * This requires a corresponding {@link GhidraProtocolHandler} extension. Helper methods within + * this utility are not provided for forming such URLs. A separate GhidraExtURL utility + * should be established if such a protocol extension is established or used. It is assumed that + * any such extension utilizing a compliant hierarchical URL which appears as an opaque URI element + * within the Ghidra URL. + *

+ * Various system path utilities are also provided in support of local Ghidra project URLs and + * {@link ProjectLocator}. */ public class GhidraURL { - // TODO: URL encoding/decoding should be used - public static final String PROTOCOL = "ghidra"; - private static final String PROTOCOL_URL_START = PROTOCOL + ":/"; - - private static Pattern IS_REMOTE_URL_PATTERN = - Pattern.compile("^" + PROTOCOL_URL_START + "/[^/].*"); // e.g., ghidra://path + private static final String PROTOCOL_URL_START = PROTOCOL + ":"; + /** + * A pattern that matches when the URL path is on the local file system. + *

+ * Pattern: + * + * ^ghidra:(/[^/]|////)(?![/]).* + * + * Explanation: + * + * ^ghidra: - starts with 'ghidra:' + * (/|////) - match a single slash or 4 slashes + * (?![/]) - with no following slashes + * .* - any other text + * + * Examples: + * + * ghidra:/path - matches + * ghidra:////path - matches + * ghidra://path - does not match + */ private static Pattern IS_LOCAL_URL_PATTERN = - Pattern.compile("^" + PROTOCOL_URL_START + "[^/].*"); // e.g., ghidra:/path + Pattern.compile("^" + PROTOCOL + ":(/|////)(?![/]).*"); // e.g., ghidra:/path or ghidra:////path + + private static Pattern STARTS_WITH_TWO_FORWARD_SLASHES_PATTERN = + Pattern.compile("^//(?![/]).*"); + + /** + * A pattern which matches a Windows path specification with a drive letter, optional '/' prefix + * and optional trailing path. + * e.g., /C:, C:, /C:/path, C:/path + */ + private static Pattern WINDOWS_DRIVE_PATH_PATTERN = Pattern.compile("^[/]{0,1}[A-Za-z]:(/.*)?"); + + /** + * A pattern that matches a UNC path which include a server and share name. + * e.g., //server/share/a/b + */ + private static Pattern UNC_PATH_PATTERN = Pattern.compile("^//(?![/]).+/(?![/]).+"); public static final String MARKER_FILE_EXTENSION = ".gpr"; public static final String PROJECT_DIRECTORY_EXTENSION = ".rep"; @@ -55,7 +102,8 @@ public class GhidraURL { /** * Determine if the specified URL refers to a local project and * it exists. - * @param url ghidra URL + * + * @param url Ghidra URL * @return true if specified URL refers to a local project and * it exists. */ @@ -65,17 +113,19 @@ public class GhidraURL { } /** - * Determine if the specified string appears to be a possible ghidra URL - * (starts with "ghidra:/"). + * Determine if the specified string appears to be a possible Ghidra URL + * (starts with "ghidra:"). + * * @param str string to be checked - * @return true if string is possible ghidra URL + * @return true if string is possible Ghidra URL */ public static boolean isGhidraURL(String str) { return str != null && str.startsWith(PROTOCOL_URL_START); } /** - * Tests if the given url is using the Ghidra protocol + * Tests if the given url is using the Ghidra protocol. + * * @param url the url to test * @return true if the url is using the Ghidra protocol */ @@ -84,66 +134,81 @@ public class GhidraURL { } /** - * Determine if URL string uses a local format (e.g., {@code ghidra:/path...}). + * Determine if URL string uses a local Ghidra project URL format (e.g., {@code ghidra:/path...}). * Extensive validation is not performed. This method is intended to differentiate * from a server URL only. + * * @param str URL string * @return true if string appears to be local Ghidra URL, else false */ - public static boolean isLocalGhidraURL(String str) { + public static boolean isLocalURL(String str) { return IS_LOCAL_URL_PATTERN.matcher(str).matches(); } /** - * Determine if URL string uses a remote server format (e.g., {@code ghidra://host...}). + * Determine if URL string uses a local Ghidra project URL format (e.g., {@code ghidra:/path...}). * Extensive validation is not performed. This method is intended to differentiate - * from a local URL only. + * from a server URL only. + * + * @param url URL + * @return true if specified URL refers to a local Ghidra project (ghidra:/path/projectName...) + */ + public static boolean isLocalURL(URL url) { + return isLocalURL(url.toExternalForm()); + } + + /** + * Determine if a URL string corresponds to a remote Ghidra server URL + * (e.g., {@code ghidra://host...}, {@code ghidra:://host...}). + * Extensive validation is not performed. This method is intended to differentiate between a + * local and remote Ghidra URL only. + * * @param str URL string * @return true if string appears to be remote server Ghidra URL, else false */ public static boolean isServerURL(String str) { - return IS_REMOTE_URL_PATTERN.matcher(str).matches(); + return isGhidraURL(str) && !isLocalURL(str); } /** - * Determine if the specified URL is a local project URL. - * No checking is performed as to the existence of the project. - * @param url ghidra URL - * @return true if specified URL refers to a local - * project (ghidra:/path/projectName...) + * Determine if the specified path is only valid on a Windows platform. Such paths contain + * either a drive specification or UNC path (e.g., C:\, /C:/, //server/..., \\server\..). + * NOTE: This does not check for existence of the specified path. + * + * @param path file path specification + * @return true if path is only valid when used on a Windows system. */ - public static boolean isLocalProjectURL(URL url) { - return isLocalGhidraURL(url.toExternalForm()); + public static boolean isWindowsOnlyPath(String path) { + String normalizedPath = path.replace('\\', '/'); + return WINDOWS_DRIVE_PATH_PATTERN.matcher(normalizedPath).matches() || + UNC_PATH_PATTERN.matcher(normalizedPath).matches(); } /** * Get the project locator which corresponds to the specified local project URL. - * Confirm local project URL with {@link #isLocalProjectURL(URL)} prior to method use. + * Confirm local project URL with {@link #isLocalURL(URL)} prior to method use. + * * @param localProjectURL local Ghidra project URL * @return project locator or null if invalid path specified * @throws IllegalArgumentException URL is not a valid - * {@link #isLocalProjectURL(URL) local project URL}. + * {@link #isLocalURL(URL) local project URL}. */ public static ProjectLocator getProjectStorageLocator(URL localProjectURL) { - if (!isLocalProjectURL(localProjectURL)) { + if (!isLocalURL(localProjectURL)) { throw new IllegalArgumentException("Invalid local Ghidra project URL"); } - String path = localProjectURL.getPath(); // assume path always starts with '/' - -// if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { // check for drive letter after leading '/' -// if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) { -// path = path.substring(1); // Strip-off leading '/' -// } -// else { -// // assume drive letter separator ':' should be removed for non-windows -// path = path.substring(0, 2) + path.substring(3); -// } -// } + URI uri = URI.create(localProjectURL.toExternalForm()); + String path = uri.getPath(); int index = path.lastIndexOf('/'); String dirPath = index != 0 ? path.substring(0, index) : "/"; + if (dirPath.startsWith("////")) { + // Prune UNC path as it appears in URL + dirPath = dirPath.substring(2); + } + String name = path.substring(index + 1); if (name.length() == 0) { return null; @@ -153,41 +218,127 @@ public class GhidraURL { } /** - * Get the shared repository name associated with a repository URL or null - * if not applicable. For ghidra URL extensions it is assumed that the first path element - * corresponds to the repository name. - * @param url ghidra URL for shared project resource - * @return repository name or null if not applicable to URL + * {@return the URL-decoded project content path contained within the query portion of + * a local Ghidra URL. The root folder will be returned if no path was specified.} + * + * @param uri GhidraURL in URI form. + * @throws URISyntaxException if a URI interpretation/parse fails */ - public static String getRepositoryName(URL url) { - if (!isServerRepositoryURL(url)) { - return null; + private static String getLocalProjectContentPath(URI uri) throws URISyntaxException { + String path = uri.getQuery(); + if (path == null) { + return "/"; } - String path = url.getPath(); if (!path.startsWith("/")) { - // handle possible ghidra protocol extension use which is assumed to encode - // repository and file path the same as standard ghidra URL. - try { - URL extensionURL = new URL(path); - path = extensionURL.getPath(); - } - catch (MalformedURLException e) { - path = ""; - } - } - path = path.substring(1); - int ix = path.indexOf("/"); - if (ix > 0) { - path = path.substring(0, ix); + throw new URISyntaxException(uri.toString(), + "Missing absolute project content path"); } return path; } + /** + * {@return the URL-decoded path contained within the specified URI.} + * NOTE: This will include the repository name included within the path. + * + * @param uri GhidraURL in URI form. + * @throws URISyntaxException if a URI interpretation/parse fails + */ + private static String getServerURIPath(URI uri) throws URISyntaxException { + String path = null; + if (uri.isOpaque()) { + // handle possible ghidra protocol extension use which is assumed to encode + // repository and file path the same as standard Ghidra URL. + String subPart = uri.getSchemeSpecificPart(); + if (StringUtils.isBlank(subPart)) { + throw new URISyntaxException(uri.toString(), "Invalid Ghidra URL"); + } + path = URI.create(subPart).getPath(); + } + else if (uri.getAuthority() != null) { + path = uri.getPath(); + } + if (StringUtils.isBlank(path)) { + throw new URISyntaxException(uri.toString(), + "Invalid ghidra repository URL - repository not specified"); + } + return path; + } + + /** + * {@return Get the URL-decoded reference/fragment from the URL or null} + *

+ * NOTE: The presence of "+" in the original reference fragment is problematic and + * requires consistent use of this method in conjunction with the URL instantiation + * methods provided by this utility class. + * + * @param url Ghidra URL + */ + public static String getDecodedReference(URL url) { + String ref = url.getRef(); + if (StringUtils.isBlank(ref)) { + return null; + } + try { + // NOTE: original "+" may appear encoded in final URL as "%252B" + ref = URLDecoder.decode(ref, "UTF-8"); + ref = ref.replace("%2B", "+"); // force double-decode of original "+" + return ref; + } + catch (UnsupportedEncodingException e) { + return null; + } + } + + /** + * Perform preliminary encode of '+' within raw ref string so that it may be preserved + * and properly decoded from {@link URI#getFragment()} by {@link #getDecodedReference(URL)}. + * Once fully encoded by URI a raw '+' will appear with a double encoding of "%252B". + * + * @param rawRef raw ref/fragment to be prepared for use with URI creation. + * @return preliminary encoding of specified raw ref string. + */ + private static String encodeRefPlus(String rawRef) { + return StringUtils.isBlank(rawRef) ? null : rawRef.replace("+", "%2B"); + } + + /** + * Get the shared repository name associated with a repository URL or null + * if not applicable. For Ghidra URL extensions it is assumed that the first path element + * corresponds to the repository name. + * + * @param url Ghidra URL for shared project resource + * @return repository name or null if not applicable to URL + */ + public static String getRepositoryName(URL url) { + if (!isServerURL(url)) { + return null; + } + + try { + URI uri = url.toURI(); + String path = getServerURIPath(uri); + if (path.length() < 2 || path.charAt(0) != '/' || path.charAt(1) == '/') { + return null; + } + path = path.substring(1); + int ix = path.indexOf("/"); + if (ix > 0) { + path = path.substring(0, ix); + } + return path; + } + catch (URISyntaxException e) { + return null; + } + } + /** * Determine if the specified URL is any type of server "repository" URL. * No checking is performed as to the existence of the server or repository. + *

* NOTE: ghidra protocol extensions are not currently supported (e.g., ghidra:http://...). - * @param url ghidra URL + * + * @param url Ghidra URL * @return true if specified URL refers to a Ghidra server * repository (ghidra://host/repositoryNAME/path...) */ @@ -195,31 +346,25 @@ public class GhidraURL { if (!isServerURL(url)) { return false; } - String path = url.getPath(); - if (StringUtils.isBlank(path)) { + try { + URI uri = url.toURI(); + String path = getServerURIPath(uri); + return path.charAt(0) == '/' && path.length() > 1 && path.charAt(1) != '/'; + } + catch (URISyntaxException e) { return false; } - if (!path.startsWith("/")) { - try { - URL extensionURL = new URL(path); - path = extensionURL.getPath(); - if (StringUtils.isBlank(path)) { - return false; - } - } - catch (MalformedURLException e) { - return false; - } - } - return path.charAt(0) == '/' && path.length() > 1 && path.charAt(1) != '/'; } /** * Determine if the specified URL is any type of supported server Ghidra URL. - * No checking is performed as to the existence of the server or repository. - * @param url ghidra URL + * If a Ghidra server extension URL is specified the corresponding {@link GhidraProtocolHandler} + * extension must be present or false will be returned. + * + * @param url Ghidra URL * @return true if specified URL refers to a Ghidra server - * repository (ghidra://host/repositoryNAME/path...) + * repository (e.g., {@code ghidra://host/repositoryNAME/path...}, + * {@code ghidra:://host/repositoryNAME/path...}) */ public static boolean isServerURL(URL url) { if (!PROTOCOL.equals(url.getProtocol())) { @@ -229,89 +374,99 @@ public class GhidraURL { } /** - * 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. If path starts 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. + * Ensure that absolute path is specified and normalize its format (e.g., Windows path + * separators are converted to '/'). An absolute path may start with a windows drive + * letter (e.g., c:\a\b, c:/a/b, /c:/a/b) or without (e.g., /a/b) or a UNC path + * (e.g., //server/share/a/b, \\server\share\a\b). + *

+ * For Windows, the lack of a drive letter is not absolute; although, for consistency with + * Linux we permit this form which on Windows will use the default drive for the process. + * If path starts 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). + *

+ * Path element naming restrictions are imposed based upon + * {@link NamingUtilities#checkName(String, String)} restrictions. These restrictions + * are imposed to ensure we can easily express the local project path in URL form. + * + * @param absolutePath path to be checked and possibly modified. + * @param isDirectory true if returned path should include a trailing '/' * @return path to be used - * @throws IllegalArgumentException if an invalid path is specified + * @throws IllegalArgumentException if an invalid path is specified based upon + * {@link NamingUtilities} restrictions. */ - 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)) { + public static String checkLocalAbsolutePath(String absolutePath, boolean isDirectory) { + + String path = absolutePath.replace('\\', '/'); + + Matcher matcher = UNC_PATH_PATTERN.matcher(path); + if (matcher.matches()) { + // Handle UNC path + checkValidProjectPath(path, 2); + if (isDirectory && !path.endsWith("/")) { + path += "/"; + } + return path; + } + else if (path.startsWith("//")) { + throw new IllegalArgumentException("Invalid UNC path: " + absolutePath); + } + + int scanIndex = 1; // char position after '/' (assumed - checked below) + + if (WINDOWS_DRIVE_PATH_PATTERN.matcher(path).matches()) { + if (!path.startsWith("/")) { path = "/" + path; } - else { - throw new IllegalArgumentException("Absolute project path required"); + if (path.length() == 3) { // e.g., '/C:' + path += '/'; // add trailing '/' immediately after ':' (directory) } - scanIndex = 3; + scanIndex = 4; // char position after '/C:/' } - else if (len >= 3 && hasDriveLetter(path, 1)) { - if (len < 4 || path.charAt(3) != '/') { - // path such as "/c:" not permitted - throw new IllegalArgumentException("Absolute project path required"); - } - scanIndex = 4; + else if (!path.startsWith("/")) { + throw new IllegalArgumentException("Absolute path required"); + } + + checkValidProjectPath(path, scanIndex); + + if (isDirectory && !path.endsWith("/")) { + path += "/"; } - checkInvalidChar("path", path, scanIndex); 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) == '/'; - } - /** - * 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 + * Check for valid project path. See {@link NamingUtilities#checkName(String, String)}). + * + * @param path project path to check (using '/' path separator). + * @param startIndex index at which to start checking immediately after root path separator + * @throws IllegalArgumentException if path 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 (ProjectLocator.DISALLOWED_CHARS.contains(c)) { - throw new IllegalArgumentException( - type + " contains invalid character: '" + c + "'"); + private static void checkValidProjectPath(String path, int startIndex) { + String str = path.substring(startIndex); + if (str.length() != 0) { + for (String s : str.split("/")) { + NamingUtilities.checkName(s, null); } } } /** - * Create a Ghidra URL from a string form of Ghidra URL or local project path. + * Create a Ghidra URL from a string form of a Ghidra URL or local project path. * This method can consume strings produced by the getDisplayString method. + * * @param projectPathOrURL {@literal project path (/)} or * string form of Ghidra URL. * @return local Ghidra project URL - * @see #getDisplayString(URL) * @throws IllegalArgumentException invalid path or URL specified */ public static URL toURL(String projectPathOrURL) { - if (!projectPathOrURL.startsWith(PROTOCOL + ":")) { + if (!isGhidraURL(projectPathOrURL)) { if (projectPathOrURL.endsWith(ProjectLocator.PROJECT_DIR_SUFFIX) || projectPathOrURL.endsWith(ProjectLocator.PROJECT_FILE_SUFFIX)) { String ext = projectPathOrURL.substring(projectPathOrURL.lastIndexOf('.')); throw new IllegalArgumentException("Project path must omit extension: " + ext); } - if (projectPathOrURL.contains("?") || projectPathOrURL.contains("#")) { - throw new IllegalArgumentException("Unsupported query/ref used with project path"); - } - projectPathOrURL = checkAbsolutePath(projectPathOrURL); + projectPathOrURL = checkLocalAbsolutePath(projectPathOrURL, false); int minSplitIndex = projectPathOrURL.charAt(2) == ':' ? 3 : 0; int splitIndex = projectPathOrURL.lastIndexOf('/'); if (splitIndex < minSplitIndex || projectPathOrURL.length() == (splitIndex + 1)) { @@ -322,130 +477,141 @@ public class GhidraURL { String projectName = projectPathOrURL.substring(splitIndex); return makeURL(location, projectName); } + + // NOTE: We must assume URL is properly encoded in its external form try { - return new URL(projectPathOrURL); + return URI.create(projectPathOrURL).toURL(); } - catch (MalformedURLException e) { - throw new IllegalArgumentException(e); + catch (Exception e) { + throw new IllegalArgumentException("Invalid Ghidra URL", e); } } + /** + * Create a new URL which is resolved from a base Ghidra project or repository URL to which + * the specified content folder or file path is added along with the optional reference. + * + * @param ghidraUrl the base Ghidra project or repository URL which will be used as the basis + * for forming a new Ghidra URL. + * @param projectFilePath an absolute folder or file path within the project (e.g., /a/b/c, + * may be null for root folder). Folder paths should end with a '/' character. + * @param ref optional location reference (may be null) which is appended to URL with a '#' + * delimiter. + * @return new resolved URL + * @throws IllegalArgumentException if an invalid Ghidra project or repository URL is specified + * or an invalid folder/file path is specified. + */ + public static URL resolve(URL ghidraUrl, String projectFilePath, String ref) { + + if (!StringUtils.isBlank(projectFilePath)) { + if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) { + throw new IllegalArgumentException("Absolute path required using '/' delimiter"); + } + checkValidProjectPath(projectFilePath, 1); + } + else { + projectFilePath = null; + } + + ref = encodeRefPlus(ref); + + Exception exc = null; + try { + URI uri = ghidraUrl.toURI(); + + if (isLocalURL(ghidraUrl)) { + String systemProjectPath = forceLocalUNCPathIfNeeded(uri.getPath()); // Preserve UNC path if needed + return new URI(PROTOCOL, null, systemProjectPath, projectFilePath, ref).toURL(); + } + + String repoName = getRepositoryName(ghidraUrl); + if (repoName != null) { + + String path = "/" + repoName; + if (projectFilePath != null) { + path += projectFilePath; + } + + if (uri.isOpaque()) { + // handle possible ghidra protocol extension use which is assumed to encode + // repository and file path the same as standard Ghidra URL. + String subPart = uri.getSchemeSpecificPart(); + if (StringUtils.isBlank(subPart)) { + throw new URISyntaxException(ghidraUrl.toExternalForm(), + "Invalid Ghidra URL"); + } + URI extURI = URI.create(subPart); + URI revisedExtURI = extURI.resolve(path); + return new URI(PROTOCOL, revisedExtURI.toURL().toExternalForm(), + ref).toURL(); + } + + // Resolve normal Ghidra server URL + uri = uri.resolve(path); + if (ref != null) { + uri = uri.resolve("#" + ref); + } + return uri.toURL(); + } + } + catch (URISyntaxException | MalformedURLException e) { + exc = e; + } + throw new IllegalArgumentException("Invalid project/repository URL: " + ghidraUrl, exc); + } + /** * Get Ghidra URL which corresponds to the local-project or repository with any * file path or query details removed. + * * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted) * @return local-project or repository URL * @throws IllegalArgumentException if URL does not specify the {@code ghidra} protocol * or does not properly identify a remote repository or local project. */ public static URL getProjectURL(URL ghidraUrl) { - if (!PROTOCOL.equals(ghidraUrl.getProtocol())) { - throw new IllegalArgumentException("ghidra protocol required"); - } - - if (isLocalProjectURL(ghidraUrl)) { - String urlStr = ghidraUrl.toExternalForm(); - int queryIx = urlStr.indexOf('?'); - if (queryIx < 0) { - return ghidraUrl; - } - urlStr = urlStr.substring(0, queryIx); - try { - return new URL(urlStr); - } - catch (MalformedURLException e) { - throw new RuntimeException(e); // unexpected - } - } - - if (isServerRepositoryURL(ghidraUrl)) { - - String path = ghidraUrl.getPath(); - // handle possible ghidra protocol extension use which is assumed to encode - // repository and file path the same as standard ghidra URL. - if (!path.startsWith("/")) { - try { - URL extensionURL = new URL(path); - path = extensionURL.getPath(); - } - catch (MalformedURLException e) { - path = "/"; - } - } - - // Truncate ghidra URL - String urlStr = ghidraUrl.toExternalForm(); - - String tail = null; - int ix = path.indexOf('/', 1); - if (ix > 0) { - // identify path tail to be removed - tail = path.substring(ix); - } - - int refIx = urlStr.indexOf('#'); - if (refIx > 0) { - urlStr = urlStr.substring(0, refIx); - } - int queryIx = urlStr.indexOf('?'); - if (queryIx > 0) { - urlStr = urlStr.substring(0, queryIx); - } - - if (tail != null) { - urlStr = urlStr.substring(0, urlStr.lastIndexOf(tail)); - } - try { - return new URL(urlStr); - } - catch (MalformedURLException e) { - // ignore - } - } - - throw new IllegalArgumentException("Invalid project/repository URL: " + ghidraUrl); + return resolve(ghidraUrl, null, null); } /** - * Get the project pathname referenced by the specified Ghidra file/folder URL. + * Get the decoded project content pathname referenced by the specified Ghidra file/folder URL. * If path is missing root folder is returned. - * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted) + *

+ * NOTE: This project content pathname should not be confused with a local project storage + * path associated with a {@link ProjectLocator} or local project Ghidra URL. + * + * @param ghidraUrl Ghidra local or remote file/folder URL (server-only URL not permitted) * @return pathname of file or folder */ public static String getProjectPathname(URL ghidraUrl) { - if (isLocalProjectURL(ghidraUrl)) { - String query = ghidraUrl.getQuery(); - return StringUtils.isBlank(query) ? "/" : query; - } + try { + URI uri = ghidraUrl.toURI(); - if (isServerRepositoryURL(ghidraUrl)) { - String path = ghidraUrl.getPath(); - // handle possible ghidra protocol extension use - if (!path.startsWith("/")) { - try { - URL extensionURL = new URL(path); - path = extensionURL.getPath(); - } - catch (MalformedURLException e) { - path = "/"; - } + if (isLocalURL(ghidraUrl)) { + return getLocalProjectContentPath(uri); } - // skip repo name (first path element) - int ix = path.indexOf('/', 1); - if (ix > 1) { - return path.substring(ix); - } - return "/"; - } - throw new IllegalArgumentException("Not a project/repository URL"); + if (isServerURL(ghidraUrl)) { + String path = getServerURIPath(uri); // URL-decoded path + + // skip repo name (first path element) + int ix = path.indexOf('/', 1); + if (ix > 1) { + return path.substring(ix); + } + return "/"; + } + } + catch (URISyntaxException e) { + // ignore + } + throw new IllegalArgumentException("Invalid project/repository URL"); } /** - * Get hostname as an IP address if possible - * @param host hostname - * @return host IP address or original host name + * {@return host name as an IP address if possible, otherwise supplied host string is returned} + * @param host host name */ private static String getHostAsIpAddress(String host) { if (!StringUtils.isBlank(host)) { @@ -460,96 +626,127 @@ public class GhidraURL { return host; } + private static String forceLocalUNCPathIfNeeded(String path) { + if (path.startsWith("//") && path.length() > 3 && path.charAt(2) != '/') { + path = "//" + path; + } + return path; + } + /** - * Force the specified URL to specify a folder. This may be neccessary when only folders + * Force the specified URL to specify a folder. This may be necessary when only folders * are supported since Ghidra permits both a folder and file to have the same name within * its parent folder. This method simply ensures that the URL path ends with a {@code /} * character if needed. - * @param ghidraUrl ghidra URL + * + * @param ghidraUrl Ghidra URL * @return ghidra folder URL - * @throws IllegalArgumentException if specified URL is niether a + * @throws IllegalArgumentException if specified URL is neither a * {@link #isServerRepositoryURL(URL) valid remote server URL} - * or {@link #isLocalProjectURL(URL) local project URL}. + * or {@link #isLocalURL(URL) local project URL}. */ public static URL getFolderURL(URL ghidraUrl) { - - if (!GhidraURL.isServerRepositoryURL(ghidraUrl) && - !GhidraURL.isLocalProjectURL(ghidraUrl)) { - throw new IllegalArgumentException("Invalid Ghidra URL: " + ghidraUrl); + String folderPath = getProjectPathname(ghidraUrl); + if (!folderPath.endsWith("/")) { + folderPath += "/"; } - - URL repoURL = GhidraURL.getProjectURL(ghidraUrl); - String path = GhidraURL.getProjectPathname(ghidraUrl); - - path = path.trim(); - if (!path.endsWith("/")) { - - // force explicit folder path - path += "/"; - - try { - if (GhidraURL.isServerRepositoryURL(ghidraUrl)) { - ghidraUrl = new URL(repoURL + path); - } - else { - ghidraUrl = new URL(repoURL + "?" + path); - } - } - catch (MalformedURLException e) { - throw new AssertionError(e); - } - } - return ghidraUrl; + return resolve(ghidraUrl, folderPath, getDecodedReference(ghidraUrl)); } /** * Get a normalized URL which eliminates use of host names and optional URL ref * which may prevent direct comparison. - * @param url ghidra URL + * + * @param url Ghidra URL * @return normalized url */ public static URL getNormalizedURL(URL url) { - String host = url.getHost(); - String revisedHost = getHostAsIpAddress(host); - if (Objects.equals(host, revisedHost) && url.getRef() == null) { - return url; // no change - } - String file = url.getPath(); - String query = url.getQuery(); - if (!StringUtils.isBlank(query)) { - file += "?" + query; + + if (!isGhidraURL(url)) { + throw new IllegalArgumentException("Ghidra URL required"); } + try { - return new URL(PROTOCOL, revisedHost, url.getPort(), file); + URI uri = url.toURI(); + + if (isLocalURL(url)) { + return new URI(PROTOCOL, null, uri.getPath(), uri.getQuery(), null).toURL(); + } + + if (uri.isOpaque()) { + // Hierarchical URL is assumed for ghidra protocol extensions + String subPart = uri.getSchemeSpecificPart(); + if (StringUtils.isBlank(subPart)) { + throw new URISyntaxException(url.toExternalForm(), "Invalid Ghidra URL"); + } + URI extUri = URI.create(subPart); + String host = extUri.getHost(); + String revisedHost = getHostAsIpAddress(host); + if (Objects.equals(host, revisedHost) && url.getRef() == null) { + return url; // no change + } + extUri = new URI(extUri.getScheme(), extUri.getUserInfo(), revisedHost, + extUri.getPort(), extUri.getPath(), extUri.getQuery(), null); + String ssp = extUri.toURL().toExternalForm(); + return new URI(PROTOCOL, ssp, null).toURL(); + } + + String host = uri.getHost(); + String revisedHost = getHostAsIpAddress(host); + if (Objects.equals(host, revisedHost) && url.getRef() == null) { + return url; // no change + } + // NOTE: Ghidra server does not support query so we strip it + return new URI(uri.getScheme(), uri.getUserInfo(), revisedHost, uri.getPort(), + uri.getPath(), null, null).toURL(); } - catch (MalformedURLException e) { - throw new RuntimeException(e); + catch (Exception e) { + throw new IllegalArgumentException("Invalid Ghidra URL", e); } } /** * Generate preferred display string for Ghidra URLs. - * Form can be parsed by the toURL method. - * @param url ghidra URL + *

+ * NOTE: The display-friendly string returned is intended for display use only and should not be + * parsed back into a URL. + * + * @param url Ghidra URL * @return formatted URL display string * @see #toURL(String) */ public static String getDisplayString(URL url) { - if (isLocalProjectURL(url) && StringUtils.isBlank(url.getQuery()) && - StringUtils.isBlank(url.getRef())) { - String path = url.getPath(); - if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { - // assume windows path - path = path.substring(1); - path = path.replace('/', '\\'); + + try { + if (isLocalURL(url) && StringUtils.isBlank(url.getQuery()) && + StringUtils.isBlank(url.getRef())) { + + URI uri = url.toURI(); + String path = uri.getPath(); + if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { + // assume windows path + path = path.substring(1); + path = path.replace('/', '\\'); + } + return path; } - return path; + } + catch (URISyntaxException e) { + // ignore } return url.toString(); } /** - * Create a URL which refers to a local Ghidra project + * Create a URL which refers to a local Ghidra project's root folder. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain the referenced root {@link DomainFolder}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param dirPath absolute path of project location directory * @param projectName name of project * @return local Ghidra project URL @@ -559,9 +756,17 @@ public class GhidraURL { } /** - * Create a URL which refers to a local Ghidra project + * Create a URL which refers to a local Ghidra project's root folder. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain the referenced root {@link DomainFolder}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param projectLocator absolute project location - * @return local Ghidra project URL + * @return local Ghidra project root folder URL * @throws IllegalArgumentException if {@code projectLocator} does not have an absolute location */ public static URL makeURL(ProjectLocator projectLocator) { @@ -570,200 +775,248 @@ public class GhidraURL { /** * Create a URL which refers to a local Ghidra project with optional project folder/file path - * and optional reference + * and optional reference. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain either the referenced {@link DomainFolder} or {@link DomainFile}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param projectLocation absolute path of project location directory * @param projectName name of project - * @param projectFilePath an absolute folder or file path within the project (e.g., /a/b/c, may be null) + * @param projectFilePath an absolute folder or file path within the project (e.g., /a/b/c, + * may be null for root folder). Folder paths should end with a '/' character. * @param ref optional location reference (may be null) which is appended to URL with a '#' - * delimiter. + * delimiter. * @return local Ghidra project URL * @throws IllegalArgumentException if an absolute projectLocation path is not specified */ public static URL makeURL(String projectLocation, String projectName, String projectFilePath, String ref) { + + // NOTE: name and path element lengths are not restricted + if (StringUtils.isBlank(projectLocation) || StringUtils.isBlank(projectName)) { throw new IllegalArgumentException("Invalid project location and/or name"); } - String path = checkAbsolutePath(projectLocation); - if (!path.endsWith("/")) { - path += "/"; - } - StringBuilder buf = new StringBuilder(PROTOCOL); - buf.append(":"); - buf.append(path); - buf.append(projectName); + NamingUtilities.checkName(projectName, "Project name"); + + String path = checkLocalAbsolutePath(projectLocation, true); + path = checkUncPathForURL(path); + path += projectName; if (!StringUtils.isBlank(projectFilePath)) { if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) { throw new IllegalArgumentException("Absolute path required using '/' delimiter"); } - buf.append("?"); - buf.append(projectFilePath); + checkValidProjectPath(projectFilePath, 1); } - if (!StringUtils.isBlank(ref)) { - buf.append("#"); - buf.append(ref); + else { + projectFilePath = null; } + try { - return new URL(buf.toString()); + return new URI(GhidraURL.PROTOCOL, null, path, projectFilePath, encodeRefPlus(ref)) + .toURL(); } - catch (MalformedURLException e) { - throw new IllegalArgumentException(e); + catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException("Unable to form local project URL", e); } } + /** + * Adjust a UNC path starting with exactly 2 forward slashes when used to form a URL. + * This handles the case where the user has typed a UNC path (e.g., \\server\share). + * We already converted backslashes to forward slashes, so we look for the converted path + * here (e.g., //server/share). If we find this case, we update the path to use 4 slashes, + * which is used in the formation of a local Ghidra URL. + * + * @param path the path to check and adjust + * @return the potentially updated path + */ + private static String checkUncPathForURL(String path) { + Matcher matcher = STARTS_WITH_TWO_FORWARD_SLASHES_PATTERN.matcher(path); + if (matcher.matches()) { + return "//" + path; + } + return path; + } + /** * Create a URL which refers to a Ghidra project with optional project file and ref. * If project locator corresponds to a transient project a server URL form will be returned. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain either the referenced {@link DomainFolder} or {@link DomainFile}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param projectLocator project locator (local or transient) - * @param projectFilePath file path (e.g., /a/b/c, may be null) + * @param projectFilePath file path (e.g., /a/b/c, may be null). Folder paths should + * end with a '/' character. * @param ref location reference (may be null) * @return local Ghidra project URL * @throws IllegalArgumentException if invalid {@code projectFilePath} specified or if URL - * instantion fails. + * instantiation fails. */ public static URL makeURL(ProjectLocator projectLocator, String projectFilePath, String ref) { - - if (projectLocator.isTransient()) { - - // Transient project corresponds to server-based repository - String serverUrl = projectLocator.getURL().toExternalForm(); - if (projectFilePath != null) { - if (!projectFilePath.startsWith("/")) { - throw new IllegalArgumentException( - "Absolute path required using '/' delimiter"); - } - serverUrl += projectFilePath; - } - if (ref != null) { - serverUrl += "#"; - serverUrl += ref; - } - try { - return new URL(serverUrl); - } - catch (MalformedURLException e) { - throw new IllegalArgumentException(e); - } - } - - // Handle local project case - return makeURL(projectLocator.getLocation(), projectLocator.getName(), projectFilePath, - ref); - } - - private static String[] splitOffName(String path) { - String name = ""; - if (!StringUtils.isBlank(path) && !path.endsWith("/")) { - int index = path.lastIndexOf('/'); - if (index >= 0) { - // last name may or may not be a folder name - name = path.substring(index + 1); - path = path.substring(0, index); - } - } - return new String[] { path, name }; + return resolve(projectLocator.getURL(), projectFilePath, ref); } /** * Create a URL which refers to Ghidra Server repository content. Path may correspond * to either a file or folder. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain either the referenced {@link DomainFolder} or {@link DomainFile}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param host server host name/address * @param port optional server port (a value <= 0 refers to the default port) * @param repositoryName repository name - * @param repositoryPath absolute folder or file path within repository. - * Folder paths should end with a '/' character. + * @param repositoryPath absolute folder or file path within repository (may be null for root folder). + * Folder paths should end with a '/' character. * @return Ghidra Server repository content URL */ public static URL makeURL(String host, int port, String repositoryName, String repositoryPath) { - String[] splitName = splitOffName(repositoryPath); - return makeURL(host, port, repositoryName, splitName[0], splitName[1], null); + return makeURL(host, port, repositoryName, repositoryPath, null); } /** * Create a URL which refers to Ghidra Server repository content. Path may correspond * to either a file or folder. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain either the referenced {@link DomainFolder} or {@link DomainFile}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param host server host name/address * @param port optional server port (a value <= 0 refers to the default port) - * @param repositoryName repository name - * @param repositoryPath absolute folder or file path within repository. - * @param ref ref or null - * Folder paths should end with a '/' character. + * @param repositoryName repository name (required) + * @param repositoryPath absolute folder or file path within repository (may be null). + * Folder paths should end with a '/' character. + * @param ref reference (may be null) * @return Ghidra Server repository content URL + * @throws IllegalArgumentException if arguments are specified which cannot be encoded into URL */ public static URL makeURL(String host, int port, String repositoryName, String repositoryPath, String ref) { - String[] splitName = splitOffName(repositoryPath); - return makeURL(host, port, repositoryName, splitName[0], splitName[1], ref); + if (StringUtils.isBlank(host)) { + throw new IllegalArgumentException("host required"); + } + if (StringUtils.isBlank(repositoryName)) { + throw new IllegalArgumentException("repository name required"); + } + NamingUtilities.checkName(repositoryName, "Repository name"); + if (port == 0 || port == GhidraServerHandle.DEFAULT_PORT) { + port = -1; + } + String path = "/" + repositoryName; + if (!StringUtils.isBlank(repositoryPath)) { + if (!repositoryPath.startsWith("/") || repositoryPath.indexOf('\\') >= 0) { + throw new IllegalArgumentException("Invalid repository path"); + } + if (repositoryPath.length() != 1) { + String checkPath = repositoryPath.substring(1); + if (checkPath.endsWith("/")) { + checkPath = checkPath.substring(0, checkPath.length() - 1); + } + checkValidProjectPath(checkPath, 0); + } + path += repositoryPath; + } + + try { + return new URI(PROTOCOL, null, host, port, path, null, encodeRefPlus(ref)).toURL(); + } + catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException(e); + } } /** * Create a URL which refers to Ghidra Server repository content. Path may correspond - * to either a file or folder. + * to either a file or folder. See {@link #makeURL(String, int, String, String, String)} + * for a slightly simpler form when working with just a project folder or file pathname. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain either the referenced {@link DomainFolder} or {@link DomainFile}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder or file object. + * * @param host server host name/address * @param port optional server port (a value <= 0 refers to the default port) - * @param repositoryName repository name - * @param repositoryFolderPath absolute folder path within repository. - * @param fileName name of a file or folder contained within the specified {@code repositoryFolderPath} + * @param repositoryName repository name (required) + * @param repositoryFolderPath absolute folder path within repository (required). + * @param childName name of a file or folder contained within the specified + * {@code repositoryFolderPath} (required) * @param ref optional URL ref or null * Folder paths should end with a '/' character. * @return Ghidra Server repository content URL * @throws IllegalArgumentException if required arguments are blank or invalid */ public static URL makeURL(String host, int port, String repositoryName, - String repositoryFolderPath, String fileName, String ref) { - if (StringUtils.isBlank(host)) { - throw new IllegalArgumentException("host required"); - } - // TODO: Need to improve checks and use of URL encoding - if (host.indexOf('@') >= 0) { // prevent user info with hostname - throw new IllegalArgumentException("invalid host name"); - } - if (StringUtils.isBlank(repositoryName)) { - throw new IllegalArgumentException("repository name required"); - } - if (port == 0 || port == GhidraServerHandle.DEFAULT_PORT) { - port = -1; - } - String path = "/" + repositoryName; - if (!StringUtils.isBlank(repositoryFolderPath)) { - if (!repositoryFolderPath.startsWith("/") || repositoryFolderPath.indexOf('\\') >= 0) { - throw new IllegalArgumentException("Invalid repository path"); - } - path += repositoryFolderPath; - if (!path.endsWith("/")) { - path += "/"; - } - } - if (!StringUtils.isBlank(fileName)) { - if (fileName.contains("/")) { - throw new IllegalArgumentException("Invalid folder/file name: " + fileName); - } - if (!path.endsWith("/")) { - path += "/"; - } - path += fileName; - } - if (!StringUtils.isBlank(ref)) { - path += "#" + ref; - } - try { - return new URL(PROTOCOL, host, port, path); - } - catch (MalformedURLException e) { - throw new IllegalArgumentException(e); + String repositoryFolderPath, String childName, String ref) { + + Objects.requireNonNull(repositoryFolderPath, "Folder path required"); + Objects.requireNonNull(childName, "Child name required"); + + String path = repositoryFolderPath; + if (!path.endsWith("/")) { + path += "/"; } + path += childName; + + return makeURL(host, port, repositoryName, path, ref); } /** - * Create a URL which refers to Ghidra Server repository and its root folder + * Create a URL which refers to Ghidra Server named repository and its root folder. + *

+ * Upon a successful URL connection, a {@link GhidraURLWrappedContent} + * content object will be provided from which {@link GhidraURLWrappedContent#getContent(Object)} + * may be invoked to obtain the repository's root {@link DomainFolder}. + *

+ * NOTE: A proper {@link GhidraURLWrappedContent#release(Object, Object)} is mandatory after + * retrieving the wrapped content folder. + * * @param host server host name/address * @param port optional server port (a value <= 0 refers to the default port) - * @param repositoryName repository name + * @param repositoryName repository name (required) * @return Ghidra Server repository URL */ public static URL makeURL(String host, int port, String repositoryName) { return makeURL(host, port, repositoryName, null); } + /** + * Create a URL which refers to Ghidra Server (i.e., no specific repository). + * Upon successful connection, the returned content type will be a + * {@link RepositoryServerAdapter} instance. + * + * @param host server host name/address + * @param port optional server port (a value <= 0 refers to the default port) + * @return Ghidra Server URL + */ + public static URL makeURL(String host, int port) { + try { + return new URI(PROTOCOL, null, host, port, null, null, null).toURL(); + } + catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java index 75517863bd..e647d0beee 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java @@ -149,7 +149,7 @@ public class GhidraURLConnection extends URLConnection { public void setReadOnly(boolean state) { if (connected) throw new IllegalStateException("Already connected"); - if (GhidraURL.isLocalProjectURL(url) && !state) { + if (GhidraURL.isLocalURL(url) && !state) { // local project write-access not supported due to inadequate cleanup/disposal strategy throw new UnsupportedOperationException("write access to local projects not supported"); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java index 7ad6a250e3..c3882c1bc1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java @@ -61,7 +61,7 @@ public abstract class GhidraURLQueryTask extends Task implements GhidraURLResult protected GhidraURLQueryTask(String title, URL ghidraUrl, Class contentClass, LinkFileControl linkFileControl) { super(title, true, false, true); - if (!GhidraURL.isLocalProjectURL(ghidraUrl) && + if (!GhidraURL.isLocalURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(ghidraUrl)) { throw new IllegalArgumentException("Unsupported URL: " + ghidraUrl); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java index 8f456f2f09..23665815e7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java @@ -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. @@ -37,10 +37,8 @@ public class Handler extends URLStreamHandler { /** * Register the "ghidra" URL protocol Handler. * Alternatively, the protocol handler can be explicitly used when instantiating - * a ghidra URL: - *

-	 *   URL url = new URL(null, "ghidra://myGhidraServer/Test", new ghidra.framework.protocol.ghidra.Handler());
-	 * 
+ * a ghidra URL (see {@link GhidraURL} utility). + *

* It is also important that a ClientAuthenticator also be registered. * @see ClientUtil#setClientAuthenticator(ghidra.framework.client.ClientAuthenticator) */ @@ -59,7 +57,7 @@ public class Handler extends URLStreamHandler { System.setProperty(PROTOCOL_HANDLER_PKGS_PROPERTY, pkgs); } - + /** * Determine if the specified url is supported and that any required * protocol extensions are recognized. @@ -70,6 +68,7 @@ public class Handler extends URLStreamHandler { if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) { return false; } + if (url.getAuthority() != null) { // assume standard ghidra URL (ghidra://...) return true; @@ -81,18 +80,33 @@ public class Handler extends URLStreamHandler { return false; } } - - private static GhidraProtocolHandler getProtocolExtensionHandler(URL url) throws MalformedURLException, NotFoundException { - String path = url.getPath(); - int index = path.indexOf("://"); - if (index <= 0) { - throw new MalformedURLException("invalid ghidra URL: " + url); + + private static String getExtensionProtocol(URL url) throws MalformedURLException { + try { + URI uri = url.toURI(); + if (uri.isOpaque()) { + String extUrl = uri.getSchemeSpecificPart(); + URI extUri = URI.create(extUrl); + String protocol = extUri.getScheme(); + if (protocol != null) { + return protocol; + } + } } - String extensionName = path.substring(0, index); + catch (URISyntaxException e) { + // ignore + } + throw new MalformedURLException("Invalid Ghidra URL"); + } + + private static GhidraProtocolHandler getProtocolExtensionHandler(URL url) + throws MalformedURLException, NotFoundException { + + String extensionName = getExtensionProtocol(url); GhidraProtocolHandler protocolHandler = findGhidraProtocolHandler(extensionName); if (protocolHandler == null) { - throw new NotFoundException("ghidra protocol extension handler (" + extensionName + - ") not found"); + throw new NotFoundException( + "ghidra protocol extension handler (" + extensionName + ") not found"); } return protocolHandler; } @@ -106,18 +120,19 @@ public class Handler extends URLStreamHandler { // Need to check for protocol extension if URL is of form ghidra: // Example: ghidra:http://host/repo/folder/filename - if (url.getAuthority() == null && !url.getPath().startsWith("/")) { + String authority = url.getAuthority(); + String path = url.getPath(); // encoded + if (authority == null && !path.startsWith("/")) { // check for protocol handler which provides access to a full repository // while specifying a specific folder/file within the repository. // The repository root is inferred from the URL path. try { GhidraProtocolHandler protocolHandler = getProtocolExtensionHandler(url); - // strip ghidra protocol specifier from URL - url = new URL(url.toExternalForm().substring(GhidraURL.PROTOCOL.length() + 1)); - return new GhidraURLConnection(url, protocolHandler); + URI extURI = URI.create(path); + return new GhidraURLConnection(extURI.toURL(), protocolHandler); } catch (NotFoundException e) { - throw new IOException("unsupported ghidra URL", e); + throw new IOException("Unsupported ghidra URL", e); } } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/ProjectLocatorTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/ProjectLocatorTest.java index 28d3db5e97..92c9df0fc6 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/ProjectLocatorTest.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/ProjectLocatorTest.java @@ -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. @@ -18,8 +18,7 @@ package ghidra.framework.model; import static org.junit.Assert.*; import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.*; import org.junit.Before; import org.junit.Test; @@ -27,6 +26,7 @@ import org.junit.Test; import generic.test.AbstractGenericTest; import ghidra.framework.Application; import ghidra.framework.OperatingSystem; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.protocol.ghidra.Handler; public class ProjectLocatorTest extends AbstractGenericTest { @@ -40,86 +40,126 @@ public class ProjectLocatorTest extends AbstractGenericTest { // Behavior of test differs when run on Windows vs Linux/Mac // + private URL toGhidraLocalURL(String path) throws MalformedURLException, URISyntaxException { + return new URI(GhidraURL.PROTOCOL, path, null).toURL(); + } + @Test - public void testPaths() throws MalformedURLException { + public void testPaths() throws Exception { ProjectLocator pl = new ProjectLocator("c:\\", "bob"); - assertEquals(new URL("ghidra:/c:/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/c:/bob"), pl.getURL()); assertEquals("/c:/", pl.getLocation()); assertEquals(new File("/c:/bob.rep"), pl.getProjectDir()); assertEquals(new File("/c:/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - assertEquals("c:/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + assertEquals("c:\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("c:\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); } pl = new ProjectLocator("/c:/", "bob"); - assertEquals(new URL("ghidra:/c:/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/c:/bob"), pl.getURL()); assertEquals("/c:/", pl.getLocation()); assertEquals(new File("/c:/bob.rep"), pl.getProjectDir()); assertEquals(new File("/c:/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - assertEquals("c:/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + assertEquals("c:\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("c:\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); + } + + pl = new ProjectLocator("c:", "bob"); + assertEquals(toGhidraLocalURL("/c:/bob"), pl.getURL()); + assertEquals("/c:/", pl.getLocation()); + assertEquals(new File("/c:/bob.rep"), pl.getProjectDir()); + assertEquals(new File("/c:/bob.gpr"), pl.getMarkerFile()); + assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); + + if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { + assertEquals("c:\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("c:\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); } pl = new ProjectLocator("c:\\a", "bob"); - assertEquals(new URL("ghidra:/c:/a/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/c:/a/bob"), pl.getURL()); assertEquals("/c:/a/", pl.getLocation()); assertEquals(new File("/c:/a/bob.rep"), pl.getProjectDir()); assertEquals(new File("/c:/a/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - assertEquals("c:/a/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/a/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + assertEquals("c:\\a\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("c:\\a\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); } pl = new ProjectLocator("c:\\a\\", "bob"); - assertEquals(new URL("ghidra:/c:/a/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/c:/a/bob"), pl.getURL()); assertEquals("/c:/a/", pl.getLocation()); assertEquals(new File("/c:/a/bob.rep"), pl.getProjectDir()); assertEquals(new File("/c:/a/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - assertEquals("c:/a/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/a/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + assertEquals("c:\\a\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("c:\\a\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); + } + + // UNC path - sensitive to execution environment, requires Windows for proper use + pl = new ProjectLocator("\\\\myserver\\myshare\\a", "bob"); + assertEquals(toGhidraLocalURL("////myserver/myshare/a/bob"), pl.getURL()); + assertEquals("//myserver/myshare/a/", pl.getLocation()); + assertEquals("bob", pl.getName()); + assertTrue(pl.isWindowsOnlyLocation()); + + if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { + assertEquals("\\\\myserver\\myshare\\a\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("\\\\myserver\\myshare\\a\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); + } + else { + // UNC path use not properly supported on non-windows platforms + assertEquals(new File("/myserver/myshare/a/bob.rep"), pl.getProjectDir()); + assertEquals(new File("/myserver/myshare/a/bob.gpr"), pl.getMarkerFile()); } pl = new ProjectLocator("\\", "bob"); - assertEquals(new URL("ghidra:/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/bob"), pl.getURL()); assertEquals("/", pl.getLocation()); assertEquals(new File("/bob.rep"), pl.getProjectDir()); assertEquals(new File("/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertFalse(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - // NOTE: Sensitive to default drive for process - assertEquals("c:/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + // NOTE: Sensitive to default drive (test assumes C: ) + assertEquals("C:\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("C:\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); } pl = new ProjectLocator("\\a\\", "bob"); - assertEquals(new URL("ghidra:/a/bob"), pl.getURL()); + assertEquals(toGhidraLocalURL("/a/bob"), pl.getURL()); assertEquals("/a/", pl.getLocation()); assertEquals(new File("/a/bob.rep"), pl.getProjectDir()); assertEquals(new File("/a/bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); + assertFalse(pl.isWindowsOnlyLocation()); if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - // NOTE: Sensitive to default drive for process - assertEquals("c:/a/bob.rep", pl.getProjectDir().getAbsolutePath()); - assertEquals("c:/a/bob.gpr", pl.getMarkerFile().getAbsolutePath()); + // NOTE: Sensitive to default drive (test assumes C: ) + assertEquals("C:\\a\\bob.rep", pl.getProjectDir().getAbsolutePath()); + assertEquals("C:\\a\\bob.gpr", pl.getMarkerFile().getAbsolutePath()); } } @Test - public void testTempPath() throws MalformedURLException { + public void testTempPath() throws Exception { String tmpPath = Application.getUserTempDirectory().getAbsolutePath().replace("\\", "/"); if (!tmpPath.startsWith("/")) { @@ -131,14 +171,14 @@ public class ProjectLocatorTest extends AbstractGenericTest { ProjectLocator pl = new ProjectLocator("", "bob"); assertEquals(tmpPath, pl.getLocation()); - assertEquals(new URL("ghidra:" + tmpPath + "bob"), pl.getURL()); + assertEquals(toGhidraLocalURL(tmpPath + "bob"), pl.getURL()); assertEquals(new File(pl.getLocation() + "bob.rep"), pl.getProjectDir()); assertEquals(new File(pl.getLocation() + "bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); pl = new ProjectLocator(null, "bob"); assertEquals(tmpPath, pl.getLocation()); - assertEquals(new URL("ghidra:" + tmpPath + "bob"), pl.getURL()); + assertEquals(toGhidraLocalURL(tmpPath + "bob"), pl.getURL()); assertEquals(new File(pl.getLocation() + "bob.rep"), pl.getProjectDir()); assertEquals(new File(pl.getLocation() + "bob.gpr"), pl.getMarkerFile()); assertEquals("bob", pl.getName()); @@ -148,8 +188,6 @@ public class ProjectLocatorTest extends AbstractGenericTest { public void testBadPaths() { // relative paths - doTestBadPath("c:", "bob"); - doTestBadPath("/c:", "bob"); doTestBadPath("a/b", "bob"); // bad paths chars diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java similarity index 63% rename from Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java rename to Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java index 2c052ed32d..3f26fa7441 100644 --- a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java @@ -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. @@ -18,7 +18,7 @@ package ghidra.framework.protocol.ghidra; import static org.junit.Assert.*; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import org.junit.Test; @@ -33,51 +33,58 @@ public class DefaultGhidraProtocolConnectorParseTest extends AbstractGenericTest @Test public void testParseURL() throws Exception { - DefaultGhidraProtocolConnector pp = - new DefaultGhidraProtocolConnector(new URL("ghidra://myhost")); + DefaultGhidraProtocolConnector pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", null, null).toURL()); try { - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost//")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "//", null).toURL()); fail(); } catch (MalformedURLException e) { // expected } - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/", null).toURL()); assertNull(pp.getRepositoryName()); assertNull(pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertNull(getInstanceField("itemPath", pp)); - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/", getInstanceField("itemPath", pp)); try { - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo//")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo//", null).toURL()); fail(); } catch (MalformedURLException e) { // expected } - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/", getInstanceField("itemPath", pp)); - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertEquals("a", pp.getFolderItemName()); assertEquals("/a", getInstanceField("itemPath", pp)); try { - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a//")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a//", null).toURL()); fail(); } catch (MalformedURLException e) { @@ -85,40 +92,46 @@ public class DefaultGhidraProtocolConnectorParseTest extends AbstractGenericTest } try { - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a///")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a///", null).toURL()); fail(); } catch (MalformedURLException e) { // expected } - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a/", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/a", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/a/", getInstanceField("itemPath", pp)); - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a/b", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/a", pp.getFolderPath()); assertEquals("b", pp.getFolderItemName()); assertEquals("/a/b", getInstanceField("itemPath", pp)); - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b/")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a/b/", null).toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/a/b", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/a/b/", getInstanceField("itemPath", pp)); try { - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b//")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a/b//", null).toURL()); fail(); } catch (MalformedURLException e) { // expected } - pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b#junk")); + pp = new DefaultGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "myhost", "/repo/a/b", "ref").toURL()); assertEquals("repo", pp.getRepositoryName()); assertEquals("/a", pp.getFolderPath()); assertEquals("b", pp.getFolderItemName()); diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java similarity index 74% rename from Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java rename to Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java index e5d8ed1c38..94c7a60d18 100644 --- a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java @@ -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. @@ -18,7 +18,7 @@ package ghidra.framework.protocol.ghidra; import static org.junit.Assert.*; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import org.junit.Test; @@ -34,38 +34,44 @@ public class DefaultLocalGhidraProtocolConnectorParseTest extends AbstractGeneri public void testParseURL() throws Exception { DefaultLocalGhidraProtocolConnector pp = - new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/C:/x/y/proj")); + new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/C:/x/y/proj", null).toURL()); assertEquals("proj", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/", getInstanceField("itemPath", pp)); - pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj")); + pp = new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/x/y/proj", null).toURL()); assertEquals("proj", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/", getInstanceField("itemPath", pp)); - pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/")); + pp = new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/x/y/proj?/", null).toURL()); assertEquals("proj", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertNull(pp.getFolderItemName()); assertEquals("/", getInstanceField("itemPath", pp)); - pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a")); + pp = new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/x/y/proj?/a", null).toURL()); assertEquals("proj", pp.getRepositoryName()); assertEquals("/", pp.getFolderPath()); assertEquals("a", pp.getFolderItemName()); assertEquals("/a", getInstanceField("itemPath", pp)); - pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a/b#ref")); + pp = new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/x/y/proj?/a/b", "ref").toURL()); assertEquals("proj", pp.getRepositoryName()); assertEquals("/a", pp.getFolderPath()); assertEquals("b", pp.getFolderItemName()); assertEquals("/a/b", getInstanceField("itemPath", pp)); try { - pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?//")); + pp = new DefaultLocalGhidraProtocolConnector( + new URI(GhidraURL.PROTOCOL, "/x/y/proj?//", null).toURL()); fail(); } catch (MalformedURLException e) { diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java new file mode 100644 index 0000000000..72bbed1ba5 --- /dev/null +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java @@ -0,0 +1,874 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.protocol.ghidra; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.*; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.framework.client.*; +import ghidra.framework.model.ProjectLocator; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; + +public class GhidraURLTest extends AbstractGenericTest { + + @Before + public void setUp() throws Exception { + Handler.registerHandler(); + } + + // makeURL(ProjectLocator) + @Test + public void testMakeLocalProjectURL() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + assertEquals("/C:/junk/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + URL ghidraUrl = GhidraURL.makeURL(loc); + URL url = toGhidraLocalURL("/C:/junk/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + assertEquals("/C:/junk/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + assertEquals("/C:/junk/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + assertEquals("/C:/junk/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/x y z/", "Test"); + assertEquals("/C:/junk/x y z/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + url = toGhidraLocalURL("/C:/junk/x y z/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + assertEquals("/a/b/", loc.getLocation()); + assertFalse(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + url = toGhidraLocalURL("/a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + // Unicode foreign language example + loc = new ProjectLocator("/\u6771\u4EAC/\u30EC\u30B9\u30C8\u30E9\u30F3", "Gr\u00FCnerTee"); + assertEquals("/\u6771\u4EAC/\u30EC\u30B9\u30C8\u30E9\u30F3/", loc.getLocation()); + assertFalse(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + url = toGhidraLocalURL("/\u6771\u4EAC/\u30EC\u30B9\u30C8\u30E9\u30F3/Gr\u00FCnerTee", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + // UNC path - limited support on Windows + loc = new ProjectLocator("\\\\myserver\\myshare\\a\\b", "Test"); + assertEquals("//myserver/myshare/a/b/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + url = toGhidraLocalURL("////myserver/myshare/a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + // UNC path - limited support on Windows + loc = new ProjectLocator("//a/b", "Test"); + assertEquals("//a/b/", loc.getLocation()); + assertTrue(loc.isWindowsOnlyLocation()); + ghidraUrl = GhidraURL.makeURL(loc); + url = toGhidraLocalURL("////a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + try { + new ProjectLocator("a/b", "Test"); + fail("relative path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + new ProjectLocator("\\\\", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + new ProjectLocator("\\\\a\\", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + new ProjectLocator("//", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(String, String) + @Test + public void testMakeLocalProjectURL2() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + URL url = toGhidraLocalURL("/C:/junk/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + ghidraUrl = GhidraURL.makeURL("C:\\junk\\", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + ghidraUrl = GhidraURL.makeURL("/C:/junk", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + ghidraUrl = GhidraURL.makeURL("/C:/junk/", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + ghidraUrl = GhidraURL.makeURL("/a/b", "Test"); + url = toGhidraLocalURL("/a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b/", "Test"); + ghidraUrl = GhidraURL.makeURL("/a/b/", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + // UNC path - limited support on Windows + loc = new ProjectLocator("\\\\a\\b", "Test"); + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test"); + url = toGhidraLocalURL("////a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + // UNC path - limited support on Windows + loc = new ProjectLocator("//a/b", "Test"); + ghidraUrl = GhidraURL.makeURL("//a/b", "Test"); + url = toGhidraLocalURL("////a/b/Test", null); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + try { + GhidraURL.makeURL("a/b/", "Test"); + fail("relative path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("\\\\", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("\\\\a\\", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("//", "Test"); + fail("incomplete network path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(ProjectLocator, String, String) + @Test + public void testMakeLocalProjectFileURL() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + + URL ghidraUrl = GhidraURL.makeURL(loc, "/a", "ref"); + URL url = toGhidraLocalURL("/C:/junk/Test", "/a", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + url = toGhidraLocalURL("/C:/junk/Test", "/a/", "ref"); + assertEquals(url, GhidraURL.getFolderURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL(loc, "/a/", "ref"); + url = toGhidraLocalURL("/C:/junk/Test", "/a/", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(url, GhidraURL.getFolderURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL(loc, "/a/x y z/", "ref"); + url = toGhidraLocalURL("/C:/junk/Test", "/a/x y z/", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(url, GhidraURL.getFolderURL(ghidraUrl)); + + // UNC project path - limited support on Windows + loc = new ProjectLocator("\\\\server\\share\\junk", "Test"); + + ghidraUrl = GhidraURL.makeURL(loc, "/a", "ref"); + url = toGhidraLocalURL("////server/share/junk/Test", "/a", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + url = toGhidraLocalURL("////server/share/junk/Test", "/a/", "ref"); + assertEquals(url, GhidraURL.getFolderURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL(loc, "/a/", "ref"); + url = toGhidraLocalURL("////server/share/junk/Test", "/a/", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(url, GhidraURL.getFolderURL(ghidraUrl)); + + try { + GhidraURL.makeURL(loc, "a/b", "ref"); + fail("relative path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(String, String, String, String) + @Test + public void testMakeLocalProjectFileURL2() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + URL url = toGhidraLocalURL("/C:/junk/Test", "/a", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + url = toGhidraLocalURL("/C:/junk/Test", "/a/", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + // Unicode foreign language example + ghidraUrl = GhidraURL.makeURL("C:\\\u6771\u4EAC", "Gr\u00FCnerTee", + "/\u30EC\u30B9\u30C8\u30E9\u30F3/", "caf\u00E9-menu"); + url = toGhidraLocalURL("/C:/\u6771\u4EAC/Gr\u00FCnerTee", + "/\u30EC\u30B9\u30C8\u30E9\u30F3/", "caf\u00E9-menu"); + assertEquals(url, ghidraUrl); + assertEquals("caf\u00E9-menu", GhidraURL.getDecodedReference(ghidraUrl)); + + try { + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "a/b", "ref"); + fail("relative path should not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + +// makeURL(String, String, String, String) + @Test + public void testMakeLocalProjectFileURL3() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk\\test.-=@ _()[]", "Test.-=@ _()[]"); + + // The ref field must allow pretty much any character + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk\\test.-=@ _()[]", "Test.-=@ _()[]", + "/a.-=@ _()[]", "ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\""); + + URL url = toGhidraLocalURL("/C:/junk/test.-=@ _()[]/Test.-=@ _()[]", "/a.-=@ _()[]", + "ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\""); + assertEquals(url, ghidraUrl); + assertEquals("ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\"", + GhidraURL.getDecodedReference(ghidraUrl)); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + try { + GhidraURL.makeURL("C:\\junk\\test+", "Test", "/a", "ref"); + fail("The '+' character is not permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("C:\\junk\\test", "Test+", "/a", "ref"); + fail("The '+' character is not permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("C:\\junk\\test", "Test", "/a+", "ref"); + fail("The '+' character is not permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + } + + // makeURL(String, int) + @Test + public void testMakeServerURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123); + URL url = toGhidraServerURL("localhost", 123, null, null); + assertEquals(url, ghidraUrl); + } + + // makeURL(String, int, String) + @Test + public void testMakeServerRepoURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + URL url = toGhidraServerURL("localhost", 123, "Test", null); + assertEquals(url, ghidraUrl); + } + + // makeURL(String, int, String, String) + @Test + public void testMakeServerRepoFileURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); + URL url = toGhidraServerURL("localhost", 123, "Test", "/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); + url = toGhidraServerURL("localhost", 123, "Test", "/foo"); + assertEquals(url, ghidraUrl); + + } + + // makeURL(String, int, String, String, String, String) + @Test + public void testMakeServerRepoFileURL2() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/", "foo", null); + URL url = toGhidraServerURL("localhost", 123, "Test", "/foo"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/", "foo/", null); + url = toGhidraServerURL("localhost", 123, "Test", "/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + url = toGhidraServerURL("localhost", 123, "Test", "/foo/bar", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + // Unicode foreign language example + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Gr\u00FCnerTee", "/\u6771\u4EAC/", + "\u30EC\u30B9\u30C8\u30E9\u30F3", "caf\u00E9-menu"); + url = toGhidraServerURL("localhost", 123, "Gr\u00FCnerTee", + "/\u6771\u4EAC/\u30EC\u30B9\u30C8\u30E9\u30F3", "caf\u00E9-menu"); + assertEquals(url, ghidraUrl); + assertEquals("caf\u00E9-menu", GhidraURL.getDecodedReference(ghidraUrl)); + } + +// makeURL(String, int, String, String) + @Test + public void testMakeServerRepoFileURL3() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); + URL url = toGhidraServerURL("localhost", 123, "Test", "/foo"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); + url = toGhidraServerURL("localhost", 123, "Test", "/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar", "ref"); + url = toGhidraServerURL("localhost", 123, "Test", "/foo/bar", "ref"); + assertEquals(url, ghidraUrl); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar"); + url = toGhidraServerURL("localhost", 123, "Test", "/foo/bar"); + assertEquals(url, ghidraUrl); + } + +// makeURL(String, int, String, String) + @Test + public void testMakeServerRepoFileURL4() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test.-=@ _()[]", "/foo.-=@ _()[]"); + URL url = toGhidraServerURL("localhost", 123, "Test.-=@ _()[]", "/foo.-=@ _()[]"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test.-=@ _()[]", "/foo.-=@ _()[]/"); + url = toGhidraServerURL("localhost", 123, "Test.-=@ _()[]", "/foo.-=@ _()[]/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test.-=@ _()[]", "/foo/bar.-=@ _()[]", + "ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\""); + url = toGhidraServerURL("localhost", 123, "Test.-=@ _()[]", "/foo/bar.-=@ _()[]", + "ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\""); + assertEquals(url, ghidraUrl); + assertEquals("ref .-=@ _()[]~!@#$%^&*+<>?/\\,`|\'\"", + GhidraURL.getDecodedReference(ghidraUrl)); + + try { + GhidraURL.makeURL("localhost", 123, "Test+", "/foo"); + fail("The '+' character is not permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + + try { + GhidraURL.makeURL("localhost", 123, "Test", "/foo+"); + fail("The '+' character is not permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // getProjectStorageLocator(URL) + @Test + public void testGetProjectStorageLocator() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + URL ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + } + + // isLocalURL(URL) + @Test + public void testIsLocalProjectURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertTrue(GhidraURL.isLocalURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertTrue(GhidraURL.isLocalURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertTrue(GhidraURL.isLocalURL(ghidraUrl)); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test"); + assertTrue(GhidraURL.isLocalURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertFalse(GhidraURL.isLocalURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertFalse(GhidraURL.isLocalURL(ghidraUrl)); + } + + // isServerRepositoryURL(URL) + @Test + public void testIsServerRepositoryURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); + assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); + assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = toGhidraServerURL("localhost", 123, "", null); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + } + + // isServerURL(URL) + @Test + public void testIsServerURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:/junk", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:/junk", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:/", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:/", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/c:", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = toGhidraServerURL("localhost", 123, "", null); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + } + + // toURL(String) + @Test + public void testToURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals(ghidraUrl, GhidraURL.toURL("C:\\junk\\Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("/C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a/#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test", "/a", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:////a/b/Test?/a#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test", "/a/", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:////a/b/Test?/a/#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/x/y/Test?/a/#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + assertEquals("ref", GhidraURL.getDecodedReference(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref+123"); + // GhidraURL.toURL requires external URL form with double-encoding for '+' in ref + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra://localhost:123/Test/foo/bar#ref%252B123")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + // Unicode foreign language example + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Gr\u00FCnerTee", "/\u6771\u4EAC/", + "\u30EC\u30B9\u30C8\u30E9\u30F3", "caf\u00E9-menu"); + assertEquals(ghidraUrl, GhidraURL.toURL( + "ghidra://localhost:123/Gr\u00FCnerTee/\u6771\u4EAC/\u30EC\u30B9\u30C8\u30E9\u30F3#caf\u00E9-menu")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + } + + @Test + public void testGetProjectURL() throws Exception { + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals(toGhidraLocalURL("/C:/junk/Test", null), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals(toGhidraLocalURL("/C:/junk/Test", null), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertEquals(toGhidraLocalURL("/x/y/Test", null), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertEquals(toGhidraServerURL("localhost", 123, "Test", null), + GhidraURL.getProjectURL(ghidraUrl)); + assertEquals(toGhidraServerURL("localhost", 123, "Test", null), ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals(toGhidraServerURL("localhost", 123, "Test", null), + GhidraURL.getProjectURL(ghidraUrl)); + assertEquals(toGhidraServerURL("localhost", 123, "Test", "/foo/bar", "ref"), ghidraUrl); + + ghidraUrl = toGhidraServerURL("localhost", 123, "", null); + try { + GhidraURL.getProjectURL(ghidraUrl); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // getDisplayString(URL) + @Test + public void testGetDisplayString() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals("C:\\junk\\Test", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals("ghidra:/x/y/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + assertEquals(ghidraUrl.toString(), GhidraURL.getDisplayString(ghidraUrl)); + + } + + // getNormalizedURL(URL) + @Test + public void testNormalizedURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals("ghidra:/C:/junk/Test", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals("ghidra:/x/y/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + assertEquals("ghidra://127.0.0.1:123/Test/foo/bar", + GhidraURL.getNormalizedURL(ghidraUrl).toString()); + } + + @Test + public void testTransientProjectURL() throws Exception { + // Dummy class implementations (see below) are used to stub objects required to establish + // transient project for URL verification testing only + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + DummyGhidraProtocolConnector dummyRepoConnector = + new DummyGhidraProtocolConnector(ghidraUrl); + TransientProjectManager transientProjectManager = + TransientProjectManager.getTransientProjectManager(); + try { + TransientProjectData transientProject = + transientProjectManager.getTransientProject(dummyRepoConnector, true); + ProjectLocator projectLocator = transientProject.getProjectLocator(); + assertTrue(GhidraURL.isServerRepositoryURL(projectLocator.getURL())); + + // Transient project will result in server URL + URL serverUrl = GhidraURL.makeURL(projectLocator, "/a/b/c", "ref"); + URL url = toGhidraServerURL("localhost", 123, "Test", "/a/b/c", "ref"); + assertEquals(url, serverUrl); + } + finally { + transientProjectManager.dispose(); + } + } + + @Test + public void testResolve() { + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals(GhidraURL.makeURL("C:\\junk", "Test", "/a/b", "ref"), + GhidraURL.resolve(ghidraUrl, "/a/b", "ref")); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals(GhidraURL.makeURL("C:\\junk", "Test", "/x/y/", "refX"), + GhidraURL.resolve(ghidraUrl, "/x/y/", "refX")); + + // Windows UNC path + ghidraUrl = GhidraURL.makeURL("\\\\a\\b", "Test"); + assertEquals(GhidraURL.makeURL("\\\\a\\b", "Test", "/x/y", "ref"), + GhidraURL.resolve(ghidraUrl, "/x/y", "ref")); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertEquals(GhidraURL.makeURL("/x/y", "Test", "/x/y/", "refX"), + GhidraURL.resolve(ghidraUrl, "/x/y/", "refX")); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertEquals(GhidraURL.makeURL("localhost", 123, "Test", "/x/y/", "ref"), + GhidraURL.resolve(ghidraUrl, "/x/y/", "ref")); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals(GhidraURL.makeURL("localhost", 123, "Test", "/x/y/", "refX"), + GhidraURL.resolve(ghidraUrl, "/x/y/", "refX")); + + } + + private static class DummyGhidraProtocolConnector extends GhidraProtocolConnector { + + private URL repositoryURL; + private DummyRepositoryAdapter repoAdapter; + + DummyGhidraProtocolConnector(URL repositoryURL) throws MalformedURLException { + super(repositoryURL); + this.repositoryURL = repositoryURL; + repoAdapter = new DummyRepositoryAdapter(); + } + + @Override + protected URL getRepositoryRootGhidraURL() { + return repositoryURL; + } + + @Override + public StatusCode connect(boolean readOnly) throws IOException { + return StatusCode.OK; + } + + @Override + public boolean isReadOnly() throws NotConnectedException { + return true; + } + + @Override + public RepositoryAdapter getRepositoryAdapter() { + return repoAdapter; + } + + } + + private static class DummyRepositoryAdapter extends RepositoryAdapter { + DummyRepositoryAdapter() { + super(new DummyRepositoryServerAdapter(), "test"); + } + + @Override + public boolean isConnected() { + return true; + } + } + + private static class DummyRepositoryServerAdapter extends RepositoryServerAdapter { + DummyRepositoryServerAdapter() { + super(null, null); + } + } + + private URL toGhidraLocalURL(String path, String projectFilePath) + throws MalformedURLException, URISyntaxException { + return new URI(GhidraURL.PROTOCOL, null, path, projectFilePath, null).toURL(); + } + + private URL toGhidraLocalURL(String path, String projectFilePath, String ref) + throws MalformedURLException, URISyntaxException { + if (ref != null) { + ref = ref.replace("+", "%2B"); // force encoding of "+" + } + return new URI(GhidraURL.PROTOCOL, null, path, projectFilePath, ref).toURL(); + } + + private URL toGhidraServerURL(String host, int port, String repo, String path) + throws MalformedURLException, URISyntaxException { + if (repo == null && path == null) { + return new URI(GhidraURL.PROTOCOL, null, host, port, null, null, null).toURL(); + } + return toGhidraServerURL(host, port, repo, path, null); + } + + private URL toGhidraServerURL(String host, int port, String repo, String path, String ref) + throws MalformedURLException, URISyntaxException { + String repoAndPath = "/" + repo; + if (path != null) { + repoAndPath += path; + } + if (ref != null) { + ref = ref.replace("+", "%2B"); // force encoding of "+" + } + return new URI(GhidraURL.PROTOCOL, null, host, port, repoAndPath, null, ref).toURL(); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java index 9bd51ec1fa..13a7d86b66 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java @@ -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. @@ -237,7 +237,7 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { setupDefaultTestTool(project); URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, - ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, null, null); + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER); ToolServices toolServices = project.getToolServices(); PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl); diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java deleted file mode 100644 index 72686a2b8b..0000000000 --- a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java +++ /dev/null @@ -1,499 +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.framework.protocol.ghidra; - -import static org.junit.Assert.*; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import org.junit.Before; -import org.junit.Test; - -import generic.test.AbstractGenericTest; -import ghidra.framework.client.*; -import ghidra.framework.model.ProjectLocator; -import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; - -public class GhidraURLTest extends AbstractGenericTest { - - @Before - public void setUp() throws Exception { - Handler.registerHandler(); - } - - // makeURL(ProjectLocator) - @Test - public void testMakeLocalProjectURL() throws Exception { - ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); - URL ghidraUrl = GhidraURL.makeURL(loc); - URL url = new URL("ghidra:/C:/junk/Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("C:\\junk\\", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk/", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - url = new URL("ghidra:/a/b/Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b/", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - try { - loc = new ProjectLocator("a/b", "Test"); - fail("relative path shold not be permitted"); - } - catch (IllegalArgumentException e) { - // expected - } - } - - // makeURL(String, String) - @Test - public void testMakeLocalProjectURL2() throws Exception { - ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - URL url = new URL("ghidra:/C:/junk/Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("C:\\junk\\", "Test"); - ghidraUrl = GhidraURL.makeURL("C:\\junk\\", "Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk", "Test"); - ghidraUrl = GhidraURL.makeURL("/C:/junk", "Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk/", "Test"); - ghidraUrl = GhidraURL.makeURL("/C:/junk/", "Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b", "Test"); - ghidraUrl = GhidraURL.makeURL("/a/b", "Test"); - url = new URL("ghidra:/a/b/Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b/", "Test"); - ghidraUrl = GhidraURL.makeURL("/a/b/", "Test"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - try { - ghidraUrl = GhidraURL.makeURL("a/b/", "Test"); - fail("relative path shold not be permitted"); - } - catch (IllegalArgumentException e) { - // expected - } - } - - // makeURL(ProjectLocator, String, String) - @Test - public void testMakeLocalProjectFileURL() throws Exception { - ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); - - URL ghidraUrl = GhidraURL.makeURL(loc, "/a", "ref"); - URL url = new URL("ghidra:/C:/junk/Test?/a#ref"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL(loc, "/a/", "ref"); - url = new URL("ghidra:/C:/junk/Test?/a/#ref"); - assertEquals(url, ghidraUrl); - - try { - ghidraUrl = GhidraURL.makeURL(loc, "a/b", "ref"); - fail("relative path shold not be permitted"); - } - catch (IllegalArgumentException e) { - // expected - } - } - - // makeURL(String, String, String, String) - @Test - public void testMakeLocalProjectFileURL2() throws Exception { - ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); - - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - URL url = new URL("ghidra:/C:/junk/Test?/a#ref"); - assertEquals(url, ghidraUrl); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); - url = new URL("ghidra:/C:/junk/Test?/a/#ref"); - assertEquals(url, ghidraUrl); - - try { - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "a/b", "ref"); - fail("relative path shold not be permitted"); - } - catch (IllegalArgumentException e) { - // expected - } - } - - // makeURL(String, int, String) - @Test - public void testMakeServerRepoURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - URL url = new URL("ghidra", "localhost", 123, "/Test"); - assertEquals(url, ghidraUrl); - } - - // makeURL(String, int, String, String) - @Test - public void testMakeServerRepoFileURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); - URL url = new URL("ghidra", "localhost", 123, "/Test/foo/"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); - url = new URL("ghidra", "localhost", 123, "/Test/foo"); - assertEquals(url, ghidraUrl); - - } - - // makeURL(String, int, String, String, String, String) - @Test - public void testMakeServerRepoFileURL2() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", null, null); - URL url = new URL("ghidra", "localhost", 123, "/Test/foo/"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", null, null); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); - url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); - assertEquals(url, ghidraUrl); - } - -// makeURL(String, int, String, String) - @Test - public void testMakeServerRepoFileURL3() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); - URL url = new URL("ghidra", "localhost", 123, "/Test/foo"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); - url = new URL("ghidra", "localhost", 123, "/Test/foo/"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar", "ref"); - url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref"); - assertEquals(url, ghidraUrl); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar"); - url = new URL("ghidra", "localhost", 123, "/Test/foo/bar"); - assertEquals(url, ghidraUrl); - } - - // getProjectStorageLocator(URL) - @Test - public void testGetProjectStorageLocator() throws Exception { - ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); - URL ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("C:\\junk\\", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/C:/junk/", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - - loc = new ProjectLocator("/a/b/", "Test"); - ghidraUrl = GhidraURL.makeURL(loc); - assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); - } - - // isLocalProjectURL(URL) - @Test - public void testIsLocalProjectURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); - assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); - assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl)); - } - - // isServerRepositoryURL(URL) - @Test - public void testIsServerRepositoryURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertNull(GhidraURL.getRepositoryName(ghidraUrl)); - assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertNull(GhidraURL.getRepositoryName(ghidraUrl)); - assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); - assertNull(GhidraURL.getRepositoryName(ghidraUrl)); - assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); - assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); - assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); - assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); - - ghidraUrl = new URL("ghidra", "localhost", 123, ""); - assertNull(GhidraURL.getRepositoryName(ghidraUrl)); - assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); - } - - // isServerURL(URL) - @Test - public void testIsServerURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertFalse(GhidraURL.isServerURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertFalse(GhidraURL.isServerURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); - assertFalse(GhidraURL.isServerURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - assertTrue(GhidraURL.isServerURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); - assertTrue(GhidraURL.isServerURL(ghidraUrl)); - - ghidraUrl = new URL("ghidra", "localhost", 123, ""); - assertTrue(GhidraURL.isServerURL(ghidraUrl)); - } - - // toURL(String) - @Test - public void testToURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertEquals(ghidraUrl, GhidraURL.toURL("C:\\junk\\Test")); - assertEquals(ghidraUrl, GhidraURL.toURL("/C:/junk/Test")); - assertEquals(ghidraUrl, GhidraURL.toURL("C:/junk/Test")); - assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test")); - assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); - assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a#ref")); - assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); - assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); - assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a/#ref")); - assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); - assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); - assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/x/y/Test?/a/#ref")); - assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); - assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); - assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); - assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); - - } - - @Test - public void testGetProjectURL() throws Exception { - - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); - assertEquals(new URL("ghidra:/x/y/Test"), GhidraURL.getProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); - assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl)); - - ghidraUrl = new URL("ghidra", "localhost", 123, ""); - try { - GhidraURL.getProjectURL(ghidraUrl); - fail("Expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) { - // expected - } - - } - - // getDisplayString(URL) - @Test - public void testGetDisplayString() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertEquals("C:\\junk\\Test", GhidraURL.getDisplayString(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertEquals("ghidra:/C:/junk/Test?/a#ref", GhidraURL.getDisplayString(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); - assertEquals("ghidra:/C:/junk/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); - assertEquals("ghidra:/x/y/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); - assertEquals(ghidraUrl.toString(), GhidraURL.getDisplayString(ghidraUrl)); - - } - - // getNormalizedURL(URL) - @Test - public void testNormalizedURL() throws Exception { - URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); - assertEquals("ghidra:/C:/junk/Test", GhidraURL.getNormalizedURL(ghidraUrl).toString()); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); - assertEquals("ghidra:/C:/junk/Test?/a", GhidraURL.getNormalizedURL(ghidraUrl).toString()); - - ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); - assertEquals("ghidra:/C:/junk/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); - - ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); - assertEquals("ghidra:/x/y/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); - - ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); - assertEquals("ghidra://127.0.0.1:123/Test/foo/bar", - GhidraURL.getNormalizedURL(ghidraUrl).toString()); - } - - @Test - public void testTransientProjectURL() throws Exception { - // Dummy class implementations (see below) are used to stub objects required to establish - // transient project for URL verification testing only - URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); - DummyGhidraProtocolConnector dummyRepoConnector = - new DummyGhidraProtocolConnector(ghidraUrl); - TransientProjectManager transientProjectManager = - TransientProjectManager.getTransientProjectManager(); - try { - TransientProjectData transientProject = - transientProjectManager.getTransientProject(dummyRepoConnector, true); - ProjectLocator projectLocator = transientProject.getProjectLocator(); - assertTrue(GhidraURL.isServerRepositoryURL(projectLocator.getURL())); - } - finally { - transientProjectManager.dispose(); - } - } - - private static class DummyGhidraProtocolConnector extends GhidraProtocolConnector { - - private URL repositoryURL; - private DummyRepositoryAdapter repoAdapter; - - DummyGhidraProtocolConnector(URL repositoryURL) throws MalformedURLException { - super(repositoryURL); - this.repositoryURL = repositoryURL; - repoAdapter = new DummyRepositoryAdapter(); - } - - @Override - protected URL getRepositoryRootGhidraURL() { - return repositoryURL; - } - - @Override - public StatusCode connect(boolean readOnly) throws IOException { - return StatusCode.OK; - } - - @Override - public boolean isReadOnly() throws NotConnectedException { - return true; - } - - @Override - public RepositoryAdapter getRepositoryAdapter() { - return repoAdapter; - } - - } - - private static class DummyRepositoryAdapter extends RepositoryAdapter { - DummyRepositoryAdapter() { - super(new DummyRepositoryServerAdapter(), "test"); - } - - @Override - public boolean isConnected() { - return true; - } - } - - private static class DummyRepositoryServerAdapter extends RepositoryServerAdapter { - DummyRepositoryServerAdapter() { - super(null, null); - } - } -}