From 0ae4aa9a0f34f27f028b5d5b26b5255acfa435ef Mon Sep 17 00:00:00 2001
From: dotasek
Date: Wed, 15 Mar 2023 16:20:51 -0400
Subject: [PATCH 1/2] Start pathbuilder work
---
.../org/hl7/fhir/utilities/PathBuilder.java | 149 ++++++++++++++++++
.../org/hl7/fhir/utilities/Utilities.java | 64 +-------
2 files changed, 155 insertions(+), 58 deletions(-)
create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
new file mode 100644
index 000000000..ef6c9fc47
--- /dev/null
+++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
@@ -0,0 +1,149 @@
+package org.hl7.fhir.utilities;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.With;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class PathBuilder {
+
+ @With
+ private final String requiredTarget;
+
+ @With
+ private final boolean requireNonNullNonEmptyFirstEntry;
+
+ @With
+ private final boolean requireNonRootFirstEntry;
+
+ @With
+ private final boolean requirePathIsChildOfTarget;
+
+ public static PathBuilder getPathBuilder() {
+ return new PathBuilder(null, true, true, true);
+ }
+
+ public String buildPath(String... args) {
+
+ checkNonNullNonEmptyFirstEntry(args);
+ checkNonRootFirstEntry(args);
+
+ StringBuilder stringBuilder = new StringBuilder();
+ boolean argIsNotEmptyOrNull = false;
+
+ boolean first = true;
+ for (String arg : args) {
+ if (first && arg == null)
+ continue;
+ first = false;
+ if (!argIsNotEmptyOrNull)
+ argIsNotEmptyOrNull = !Utilities.noString(arg);
+ else if (!stringBuilder.toString().endsWith(File.separator))
+ stringBuilder.append(File.separator);
+ String a = arg;
+ if (stringBuilder.length() == 0) {
+ a = replaceVariables(a);
+ }
+ a = a.replace("\\", File.separator);
+ a = a.replace("/", File.separator);
+ if (stringBuilder.length() > 0 && a.startsWith(File.separator))
+ a = a.substring(File.separator.length());
+
+ while (a.startsWith(".." + File.separator)) {
+ if (stringBuilder.length() == 0) {
+ stringBuilder = new StringBuilder(Paths.get(".").toAbsolutePath().normalize().toString());
+ } else {
+ String p = stringBuilder.toString().substring(0, stringBuilder.length() - 1);
+ if (!p.contains(File.separator)) {
+ stringBuilder = new StringBuilder();
+ } else {
+ stringBuilder = new StringBuilder(p.substring(0, p.lastIndexOf(File.separator)) + File.separator);
+ }
+ }
+ a = a.substring(3);
+ }
+ if ("..".equals(a)) {
+ int i = stringBuilder.substring(0, stringBuilder.length() - 1).lastIndexOf(File.separator);
+ stringBuilder = new StringBuilder(stringBuilder.substring(0, i + 1));
+ } else
+ stringBuilder.append(a);
+ }
+ checkPathIsChildOfTarget(stringBuilder.toString(), args);
+ return stringBuilder.toString();
+ }
+
+ private void checkPathIsChildOfTarget(String path, String[] args) {
+ if (!requirePathIsChildOfTarget) {
+ return;
+ }
+
+ final String target = requiredTarget != null
+ ? requiredTarget
+ : args[0];
+
+ if (!Path.of(path).normalize().startsWith(Path.of(replaceVariables(target)).normalize())) {
+ throw new RuntimeException("Computed path does not start with first element: " + String.join(", ", args));
+ }
+ }
+
+ private void checkNonRootFirstEntry(String[] args) {
+ if (!requireNonRootFirstEntry) {
+ return;
+ }
+ if (isPathRoot(args[0])) {
+ throw new RuntimeException("First entry cannot be root: " + args[0]);
+ }
+ }
+
+ private void checkNonNullNonEmptyFirstEntry(String[] args) {
+ if (!requireNonNullNonEmptyFirstEntry) {
+ return;
+ }
+ if (args[0] == null || Utilities.noString(args[0].trim())) {
+ throw new RuntimeException("First entry cannot be null or empty");
+ }
+ }
+
+
+ private String replaceVariables(String a) {
+ if ("[tmp]".equals(a)) {
+ if (hasCTempDir()) {
+ return Utilities.C_TEMP_DIR;
+ } else if (ToolGlobalSettings.hasTempPath()) {
+ return ToolGlobalSettings.getTempPath();
+ } else {
+ return System.getProperty("java.io.tmpdir");
+ }
+ } else if ("[user]".equals(a)) {
+ return System.getProperty("user.home");
+ } else if (a.startsWith("[") && a.endsWith("]")) {
+ String ev = System.getenv(a.replace("[", "").replace("]", ""));
+ if (ev != null) {
+ return ev;
+ } else {
+ return "null";
+ }
+ }
+ return a;
+ }
+
+ protected static boolean hasCTempDir() {
+ if (!System.getProperty("os.name").toLowerCase().contains("win")) {
+ return false;
+ }
+ File tmp = new File(Utilities.C_TEMP_DIR);
+ return tmp.exists() && tmp.isDirectory() && tmp.canWrite();
+ }
+
+ protected static boolean isPathRoot(String pathString) {
+ boolean actual;
+ Path path = Path.of(pathString);
+ Path normalizedPath = path.normalize();
+ actual = normalizedPath.equals(path.getRoot());
+ return actual;
+ }
+}
diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
index ced64e622..23a1b6e1c 100644
--- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
+++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
@@ -635,20 +635,7 @@ public class Utilities {
* @throws IOException
*/
public static String path(String... args) throws IOException {
- if (args[0] == null || noString(args[0].trim())) {
- throw new RuntimeException("First entry cannot be null or empty");
- }
-
- if (isPathRoot(args[0])) {
- throw new RuntimeException("First entry cannot be root: " + args[0]);
- }
-
- String output = uncheckedPath(args);
-
- if (!Path.of(output.toString()).normalize().startsWith(Path.of(replaceVariables(args[0])).normalize())) {
- throw new RuntimeException("Computed path does not start with first element: " + String.join(", ", args));
- }
- return output.toString();
+ return PathBuilder.getPathBuilder().buildPath(args);
}
/**
@@ -664,50 +651,11 @@ public class Utilities {
* @throws IOException
*/
public static String uncheckedPath(String... args) {
- StringBuilder s = new StringBuilder();
- boolean argIsNotEmptyOrNull = false;
-
- boolean first = true;
- for (String arg : args) {
- if (first && arg == null)
- continue;
- first = false;
- if (!argIsNotEmptyOrNull)
- argIsNotEmptyOrNull = !noString(arg);
- else if (!s.toString().endsWith(File.separator))
- s.append(File.separator);
- String a = arg;
- if (s.length() == 0) {
- a = replaceVariables(a);
- }
- a = a.replace("\\", File.separator);
- a = a.replace("/", File.separator);
- if (s.length() > 0 && a.startsWith(File.separator))
- a = a.substring(File.separator.length());
-
- while (a.startsWith(".." + File.separator)) {
- if (s.length() == 0) {
- s = new StringBuilder(Paths.get(".").toAbsolutePath().normalize().toString());
- } else {
- String p = s.toString().substring(0, s.length() - 1);
- if (!p.contains(File.separator)) {
- s = new StringBuilder();
- } else {
- s = new StringBuilder(p.substring(0, p.lastIndexOf(File.separator)) + File.separator);
- }
- }
- a = a.substring(3);
- }
- if ("..".equals(a)) {
- int i = s.substring(0, s.length() - 1).lastIndexOf(File.separator);
- s = new StringBuilder(s.substring(0, i + 1));
- } else
- s.append(a);
- }
-// if (!Path.of(s.toString()).normalize().startsWith(Path.of(replaceVariables(args[0])).normalize())) {
-// throw new RuntimeException("Computed path '"+s.toString()+"' normalised to '"+Path.of(s.toString()).normalize()+"' does not start with first element: " + String.join(", ", args));
-// }
- return s.toString();
+ return PathBuilder.getPathBuilder()
+ .withRequireNonRootFirstEntry(false)
+ .withRequireNonNullNonEmptyFirstEntry(false)
+ .withRequirePathIsChildOfTarget(false)
+ .buildPath(args);
}
private static String replaceVariables(String a) {
From 3044a188d9a41b2ed8cb60d936100b5e7f7382ba Mon Sep 17 00:00:00 2001
From: dotasek
Date: Mon, 27 Mar 2023 10:41:44 -0400
Subject: [PATCH 2/2] Add PathBuilder documentation clean up code.
---
.../org/hl7/fhir/utilities/PathBuilder.java | 53 +++++++++++++++++++
.../org/hl7/fhir/utilities/Utilities.java | 48 ++---------------
2 files changed, 58 insertions(+), 43 deletions(-)
diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
index ef6c9fc47..a063bf93c 100644
--- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
+++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java
@@ -11,22 +11,75 @@ import java.nio.file.Paths;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class PathBuilder {
+ /**
+ * By default, the normalized built path must be a child of the first entry of the buildPath arguments. If a
+ * different parent is desired, this can be set via withRequiredTarget
+ */
@With
private final String requiredTarget;
+ /**
+ * By default, the first entry of the buildPath argument cannot be null or an empty string. Setting this to false will
+ * disable this check.
+ */
@With
private final boolean requireNonNullNonEmptyFirstEntry;
+ /**
+ * By default, the first entry of the buildPath argument cannot be a root directory ("/", "C:\", etc.
. Setting this to false will disable this check.
+ */
@With
private final boolean requireNonRootFirstEntry;
+ /**
+ * By default, the normalized built path must be a child of the first entry of the buildPath arguments. Setting this
+ * to false will disable this check.
+ */
@With
private final boolean requirePathIsChildOfTarget;
+ /**
+ * Returns an instance of PathBuilder with all checks enabled (recommended).
+ *
+ * @return
+ */
public static PathBuilder getPathBuilder() {
return new PathBuilder(null, true, true, true);
}
+ /**
+ * Builds a path from the passed argument strings. This path will be compatible with the local filesystem.
+ *
+ *
+ *
+ * If the args contain variables enclosed in square brackets ([ ]
), they will be replaced with values in
+ * the built path. There are several built-in variables available, listed below. Any text between square brackets that
+ * does not match these will be replaced by a matching System environment variable if one is available.
+ *
+ *
+ *
+ * Built-in variables include:
+ *
+ * - [tmp] An available temp directory (Java Temp directory,
c:\\temp, $TMPDIR, %TEMP%
, etc.)
+ * - [user] The OS user directory (~, user.home, etc)
+ *
+ *
+ *
+ *
+ * This method will run several checks by default to ensure that the built path does not point to unintended areas of
+ * the filesystem. If these checks are violated, a RuntimeException will be thrown. If needed in special cases, the
+ * behavior of these checks can be modified via the linked fluent constructor methods below.
+ *
+ *
+ * @param args entries with which to construct the filesystem path
+ * @throws RuntimeException
+ * @return a local filesystem path
+ *
+ * @see this#withRequiredTarget(String)
+ * @see this#withRequireNonNullNonEmptyFirstEntry(boolean)
+ * @see this#withRequireNonRootFirstEntry(boolean)
+ * @see this#withRequirePathIsChildOfTarget(boolean)
+ */
public String buildPath(String... args) {
checkNonNullNonEmptyFirstEntry(args);
diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
index 9368a3d1e..76371ab78 100644
--- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
+++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java
@@ -613,26 +613,15 @@ public class Utilities {
return s.toString();
}
- private static boolean isPathRoot(String pathString) {
- boolean actual;
- Path path = Path.of(pathString);
- Path normalizedPath = path.normalize();
- actual = normalizedPath.equals(path.getRoot());
- return actual;
- }
-
/**
* Composes a path string using by concatenating the passed arguments.
- * Variables such as [tmp] and [user] are replaced.
*
- * In order to prevent unintentional access to areas of the file system
- * outside of the first entry, this method will throw exceptions in situations
- * where the constructed path is at a higher level than the first entry, or
- * where the first entry is null or empty.
+ * This method enables all checks for unintended path locations.
*
* @param args
* @return
* @throws IOException
+ * @see PathBuilder#buildPath(String...)
*/
public static String path(String... args) throws IOException {
return PathBuilder.getPathBuilder().buildPath(args);
@@ -640,7 +629,6 @@ public class Utilities {
/**
* Composes a path string using by concatenating the passed arguments.
- * Variables such as [tmp] and [user] are replaced.
*
* This method does not check for unintentional access to areas of the file
* system outside of the first entry. ONLY USE THIS METHOD IN CASES WHERE YOU
@@ -649,7 +637,10 @@ public class Utilities {
* @param args
* @return
* @throws IOException
+ *
+ * @see PathBuilder#buildPath(String...)
*/
+ @Deprecated
public static String uncheckedPath(String... args) {
return PathBuilder.getPathBuilder()
.withRequireNonRootFirstEntry(false)
@@ -658,35 +649,6 @@ public class Utilities {
.buildPath(args);
}
- private static String replaceVariables(String a) {
- if ("[tmp]".equals(a)) {
- if (hasCTempDir()) {
- return C_TEMP_DIR;
- } else if (ToolGlobalSettings.hasTempPath()) {
- return ToolGlobalSettings.getTempPath();
- } else {
- return System.getProperty("java.io.tmpdir");
- }
- } else if ("[user]".equals(a)) {
- return System.getProperty("user.home");
- } else if (a.startsWith("[") && a.endsWith("]")) {
- String ev = System.getenv(a.replace("[", "").replace("]", ""));
- if (ev != null) {
- return ev;
- } else {
- return "null";
- }
- }
- return a;
- }
-
- private static boolean hasCTempDir() {
- if (!System.getProperty("os.name").toLowerCase().contains("win")) {
- return false;
- }
- File tmp = new File(C_TEMP_DIR);
- return tmp.exists() && tmp.isDirectory() && tmp.canWrite();
- }
public static String pathURL(String... args) {
StringBuilder s = new StringBuilder();