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

+ *

+ * + *

+ * 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();