Bug 573456 - [performance] Avoid File.getCanonicalPath

File.getCanonicalPath is much slower then URI.normalize under windows
Also avoid normalization if no ".." is involved.

Change-Id: I641a64d48f8acb9cf793c948d08c51ace45aec50
Signed-off-by: Joerg Kubitz <jkubitz-eclipse@gmx.de>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.framework/+/180404
Tested-by: Equinox Bot <equinox-bot@eclipse.org>
Reviewed-by: Thomas Watson <tjwatson@us.ibm.com>
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/AllTests.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/AllTests.java
index beaa871..9ac4293 100644
--- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/AllTests.java
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/AllTests.java
@@ -30,6 +30,7 @@
 		addTest(new TestSuite(ObjectPoolTestCase.class));
 		addTest(new JUnit4TestAdapter(ManifestElementTestCase.class));
 		addTest(new TestSuite(NLSTestCase.class));
+		addTest(new TestSuite(StorageUtilTestCase.class));
 		addBidiTests();
 		addLatinTests();
 	}
diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/StorageUtilTestCase.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/StorageUtilTestCase.java
new file mode 100644
index 0000000..7491382
--- /dev/null
+++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/util/StorageUtilTestCase.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2013 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.osgi.tests.util;
+
+import java.io.File;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.tests.harness.CoreTest;
+import org.eclipse.osgi.storage.StorageUtil;
+import org.junit.Test;
+
+public class StorageUtilTestCase extends CoreTest {
+
+	@Test
+	public void testRegularWindowsFileName() {
+		String[] validFilenames = { //
+				"something\\somethingelse", // normal file
+				"something/somethingelse", // normal file
+				"COM1anything", // normal file
+				"COM1/anything", // illegal directory name but normal file
+				".temp", // It is acceptable to specify a period as the first character of a
+							// name. For example, ".temp".
+				"COM56", // there is no predefined NT namespace for COM56.
+		};
+		for (String validFilename : validFilenames) {
+			assertFalse(validFilename, StorageUtil.isReservedFileName(new File(validFilename)));
+		}
+		// test reserved names according to
+		// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+		String[] invalidFilenames = { //
+				"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+				"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", // reserved names
+				"COM1.anything", "NUL.txt", // "Also avoid these names followed immediately by an extension; for
+											// example, NUL.txt is not recommended"
+				"COM1", "com1", "coM1"// case insensitive
+		};
+		boolean isWindows = Platform.getOS().equals(Platform.OS_WIN32);
+		for (String invalidFilename : invalidFilenames) {
+			assertTrue(invalidFilename, isWindows == StorageUtil.isReservedFileName(new File(invalidFilename)));
+		}
+	}
+
+}
diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/Storage.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/Storage.java
index f7be9b5..2f6d939 100644
--- a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/Storage.java
+++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/Storage.java
@@ -30,6 +30,7 @@
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
@@ -1078,18 +1079,19 @@
 		}
 
 		// if base is not null then move root to include the base
-		root = new File(root, base);
-		File result = new File(root, path);
-
-		// do the extra check to make sure the path did not escape the root path
-		try {
-			String resultCanonical = result.getCanonicalPath();
-			String rootCanonical = root.getCanonicalPath();
-			if (!resultCanonical.startsWith(rootCanonical + File.separator) && !resultCanonical.equals(rootCanonical)) {
+		File rootBase = new File(root, base);
+		File result = new File(rootBase, path);
+		if (path.contains("..")) { //$NON-NLS-1$
+			// do the extra check to make sure the path did not escape the root path
+			Path resultNormalized = result.toPath().normalize();
+			Path rootBaseNormalized = rootBase.toPath().normalize();
+			if (!resultNormalized.startsWith(rootBaseNormalized)) {
 				throw new StorageException("Invalid path: " + path); //$NON-NLS-1$
 			}
-		} catch (IOException e) {
-			throw new StorageException("Invalid path: " + path, e); //$NON-NLS-1$
+		}
+		// Additional check if it is a special device instead of a regular file.
+		if (StorageUtil.isReservedFileName(result)) {
+			throw new StorageException("Invalid filename: " + path); //$NON-NLS-1$
 		}
 		return result;
 	}
diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/StorageUtil.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/StorageUtil.java
index 6af3d74..4f4ebc3 100644
--- a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/StorageUtil.java
+++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/storage/StorageUtil.java
@@ -25,7 +25,9 @@
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
 import java.util.Dictionary;
+import java.util.HashSet;
 import java.util.Hashtable;
 import org.eclipse.osgi.internal.debug.Debug;
 import org.osgi.framework.BundleContext;
@@ -256,4 +258,44 @@
 			Debug.println("Successfully moved file: " + from + " to " + to); //$NON-NLS-1$ //$NON-NLS-2$
 		}
 	}
+
+	private static final boolean IS_WINDOWS = "\\\\.\\NUL" //$NON-NLS-1$
+			.equals(getDeviceName(new File("NUL"))); //$NON-NLS-1$
+
+	private static String getDeviceName(File file) {
+		try {
+			return file.getCanonicalPath();
+		} catch (IOException e) {
+			return null;
+		}
+	}
+
+	// reserved names according to
+	// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+	private static HashSet<String> RESERVED_NAMES = new HashSet<>(
+			Arrays.asList(new String[] { "aux", "com1", "com2", "com3", "com4", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
+					"com5", "com6", "com7", "com8", "com9", "con", "lpt1", "lpt2", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+					"lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "nul", "prn" })); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$
+
+	/** Tests whether the filename can escape path into special device **/
+	public static boolean isReservedFileName(File file) {
+		// Directory names are not checked here because illegal directory names will be
+		// handled by OS.
+		String fileName = file.getName();
+		return isReservedFileName(fileName);
+	}
+
+	private static boolean isReservedFileName(String name) {
+		// Illegal characters are not checked here because they are check by both JDK
+		// and OS. This is only a check against technical allowed but unwanted device
+		// names.
+		if (!IS_WINDOWS) { // only windows has special file names which can escape any path
+			return false;
+		}
+		int dot = name.indexOf('.');
+		// on windows, filename suffixes are not relevant to name validity
+		String basename = dot == -1 ? name : name.substring(0, dot);
+		return RESERVED_NAMES.contains(basename.toLowerCase());
+	}
+
 }