Use NIO to simplify and cleanup path processing
diff --git a/src/main/java/org/apache/commons/io/FileSystemUtils.java b/src/main/java/org/apache/commons/io/FileSystemUtils.java
index 1284285..ec8a4ea 100644
--- a/src/main/java/org/apache/commons/io/FileSystemUtils.java
+++ b/src/main/java/org/apache/commons/io/FileSystemUtils.java
@@ -22,6 +22,8 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
@@ -33,8 +35,7 @@
 /**
  * General File System utilities.
  * <p>
- * This class provides static utility methods for general file system
- * functions not provided via the JDK {@link java.io.File File} class.
+ * This class provides static utility methods for general file system functions not provided via the JDK {@link java.io.File File} class.
  * <p>
  * The current functions provided are:
  * <ul>
@@ -42,10 +43,8 @@
  * </ul>
  *
  * @since 1.1
- * @deprecated As of 2.6 deprecated without replacement. Use equivalent
- *  methods in {@link java.nio.file.FileStore} instead, e.g.
- *  {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()}
- *  or iterate over {@code FileSystems.getDefault().getFileStores()}
+ * @deprecated As of 2.6 deprecated without replacement. Use equivalent methods in {@link java.nio.file.FileStore} instead, e.g.
+ *             {@code Files.getFileStore(Paths.get("/home")).getUsableSpace()} or iterate over {@code FileSystems.getDefault().getFileStores()}
  */
 @Deprecated
 public class FileSystemUtils {
@@ -82,22 +81,13 @@
             // match
             if (osName.contains("windows")) {
                 os = WINDOWS;
-            } else if (osName.contains("linux") ||
-                    osName.contains("mpe/ix") ||
-                    osName.contains("freebsd") ||
-                    osName.contains("openbsd") ||
-                    osName.contains("irix") ||
-                    osName.contains("digital unix") ||
-                    osName.contains("unix") ||
-                    osName.contains("mac os x")) {
+            } else if (osName.contains("linux") || osName.contains("mpe/ix") || osName.contains("freebsd") || osName.contains("openbsd")
+                    || osName.contains("irix") || osName.contains("digital unix") || osName.contains("unix") || osName.contains("mac os x")) {
                 os = UNIX;
-            } else if (osName.contains("sun os") ||
-                    osName.contains("sunos") ||
-                    osName.contains("solaris")) {
+            } else if (osName.contains("sun os") || osName.contains("sunos") || osName.contains("solaris")) {
                 os = POSIX_UNIX;
                 dfPath = "/usr/xpg4/bin/df";
-            } else if (osName.contains("hp-ux") ||
-                    osName.contains("aix")) {
+            } else if (osName.contains("hp-ux") || osName.contains("aix")) {
                 os = POSIX_UNIX;
             }
 
@@ -109,30 +99,26 @@
     }
 
     /**
-     * Returns the free space on a drive or volume by invoking
-     * the command line.
-     * This method does not normalize the result, and typically returns
-     * bytes on Windows, 512 byte units on OS X and kilobytes on Unix.
-     * As this is not very useful, this method is deprecated in favor
-     * of {@link #freeSpaceKb(String)} which returns a result in kilobytes.
+     * Returns the free space on a drive or volume by invoking the command line. This method does not normalize the result, and typically returns bytes on
+     * Windows, 512 byte units on OS X and kilobytes on Unix. As this is not very useful, this method is deprecated in favor of {@link #freeSpaceKb(String)}
+     * which returns a result in kilobytes.
      * <p>
-     * Note that some OS's are NOT currently supported, including OS/390,
-     * OpenVMS.
-     * <pre>
-     * FileSystemUtils.freeSpace("C:");       // Windows
-     * FileSystemUtils.freeSpace("/volume");  // *nix
-     * </pre>
-     * The free space is calculated via the command line.
-     * It uses 'dir /-c' on Windows and 'df' on *nix.
+     * Note that some OS's are NOT currently supported, including OS/390, OpenVMS.
      *
-     * @param path  the path to get free space for, not null, not empty on Unix
+     * <pre>
+     * FileSystemUtils.freeSpace("C:"); // Windows
+     * FileSystemUtils.freeSpace("/volume"); // *nix
+     * </pre>
+     *
+     * The free space is calculated via the command line. It uses 'dir /-c' on Windows and 'df' on *nix.
+     *
+     * @param path the path to get free space for, not null, not empty on Unix
      * @return the amount of free drive space on the drive or volume
      * @throws IllegalArgumentException if the path is invalid
-     * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IllegalStateException    if an error occurred in initialization
+     * @throws IOException              if an error occurs when finding the free space
      * @since 1.1, enhanced OS support in 1.2 and 1.3
-     * @deprecated Use freeSpaceKb(String)
-     *  Deprecated from 1.3, may be removed in 2.0
+     * @deprecated Use freeSpaceKb(String) Deprecated from 1.3, may be removed in 2.0
      */
     @Deprecated
     public static long freeSpace(final String path) throws IOException {
@@ -140,16 +126,17 @@
     }
 
     /**
-     * Returns the free space for the working directory
-     * in kibibytes (1024 bytes) by invoking the command line.
+     * Returns the free space for the working directory in kibibytes (1024 bytes) by invoking the command line.
      * <p>
      * Identical to:
+     *
      * <pre>
      * freeSpaceKb(FileUtils.current().getAbsolutePath())
      * </pre>
+     *
      * @return the amount of free drive space on the drive or volume in kilobytes
      * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IOException           if an error occurs when finding the free space
      * @since 2.0
      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
      */
@@ -159,18 +146,18 @@
     }
 
     /**
-     * Returns the free space for the working directory
-     * in kibibytes (1024 bytes) by invoking the command line.
+     * Returns the free space for the working directory in kibibytes (1024 bytes) by invoking the command line.
      * <p>
      * Identical to:
+     *
      * <pre>
      * freeSpaceKb(FileUtils.current().getAbsolutePath())
      * </pre>
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     *
+     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the amount of free drive space on the drive or volume in kilobytes
      * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IOException           if an error occurs when finding the free space
      * @since 2.0
      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
      */
@@ -178,28 +165,26 @@
     public static long freeSpaceKb(final long timeout) throws IOException {
         return freeSpaceKb(FileUtils.current().getAbsolutePath(), timeout);
     }
+
     /**
-     * Returns the free space on a drive or volume in kibibytes (1024 bytes)
-     * by invoking the command line.
-     * <pre>
-     * FileSystemUtils.freeSpaceKb("C:");       // Windows
-     * FileSystemUtils.freeSpaceKb("/volume");  // *nix
-     * </pre>
-     * The free space is calculated via the command line.
-     * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
-     * <p>
-     * In order to work, you must be running Windows, or have an implementation of
-     * Unix df that supports GNU format when passed -k (or -kP). If you are going
-     * to rely on this code, please check that it works on your OS by running
-     * some simple tests to compare the command line with the output from this class.
-     * If your operating system isn't supported, please raise a JIRA call detailing
-     * the exact result from df -k and as much other detail as possible, thanks.
+     * Returns the free space on a drive or volume in kibibytes (1024 bytes) by invoking the command line.
      *
-     * @param path  the path to get free space for, not null, not empty on Unix
+     * <pre>
+     * FileSystemUtils.freeSpaceKb("C:"); // Windows
+     * FileSystemUtils.freeSpaceKb("/volume"); // *nix
+     * </pre>
+     *
+     * The free space is calculated via the command line. It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
+     * <p>
+     * In order to work, you must be running Windows, or have an implementation of Unix df that supports GNU format when passed -k (or -kP). If you are going to
+     * rely on this code, please check that it works on your OS by running some simple tests to compare the command line with the output from this class. If
+     * your operating system isn't supported, please raise a JIRA call detailing the exact result from df -k and as much other detail as possible, thanks.
+     *
+     * @param path the path to get free space for, not null, not empty on Unix
      * @return the amount of free drive space on the drive or volume in kilobytes
      * @throws IllegalArgumentException if the path is invalid
-     * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IllegalStateException    if an error occurred in initialization
+     * @throws IOException              if an error occurs when finding the free space
      * @since 1.2, enhanced OS support in 1.3
      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
      */
@@ -209,29 +194,25 @@
     }
 
     /**
-     * Returns the free space on a drive or volume in kibibytes (1024 bytes)
-     * by invoking the command line.
-     * <pre>
-     * FileSystemUtils.freeSpaceKb("C:");       // Windows
-     * FileSystemUtils.freeSpaceKb("/volume");  // *nix
-     * </pre>
-     * The free space is calculated via the command line.
-     * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
-     * <p>
-     * In order to work, you must be running Windows, or have an implementation of
-     * Unix df that supports GNU format when passed -k (or -kP). If you are going
-     * to rely on this code, please check that it works on your OS by running
-     * some simple tests to compare the command line with the output from this class.
-     * If your operating system isn't supported, please raise a JIRA call detailing
-     * the exact result from df -k and as much other detail as possible, thanks.
+     * Returns the free space on a drive or volume in kibibytes (1024 bytes) by invoking the command line.
      *
-     * @param path  the path to get free space for, not null, not empty on Unix
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     * <pre>
+     * FileSystemUtils.freeSpaceKb("C:"); // Windows
+     * FileSystemUtils.freeSpaceKb("/volume"); // *nix
+     * </pre>
+     *
+     * The free space is calculated via the command line. It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
+     * <p>
+     * In order to work, you must be running Windows, or have an implementation of Unix df that supports GNU format when passed -k (or -kP). If you are going to
+     * rely on this code, please check that it works on your OS by running some simple tests to compare the command line with the output from this class. If
+     * your operating system isn't supported, please raise a JIRA call detailing the exact result from df -k and as much other detail as possible, thanks.
+     *
+     * @param path    the path to get free space for, not null, not empty on Unix
+     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the amount of free drive space on the drive or volume in kilobytes
      * @throws IllegalArgumentException if the path is invalid
-     * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IllegalStateException    if an error occurred in initialization
+     * @throws IOException              if an error occurs when finding the free space
      * @since 2.0
      * @deprecated As of 2.6 deprecated without replacement. Please use {@link java.nio.file.FileStore#getUsableSpace()}.
      */
@@ -251,24 +232,23 @@
     }
 
     /**
-     * Returns the free space on a drive or volume in a cross-platform manner.
-     * Note that some OS's are NOT currently supported, including OS/390.
-     * <pre>
-     * FileSystemUtils.freeSpace("C:");  // Windows
-     * FileSystemUtils.freeSpace("/volume");  // *nix
-     * </pre>
-     * The free space is calculated via the command line.
-     * It uses 'dir /-c' on Windows and 'df' on *nix.
+     * Returns the free space on a drive or volume in a cross-platform manner. Note that some OS's are NOT currently supported, including OS/390.
      *
-     * @param path  the path to get free space for, not null, not empty on Unix
-     * @param os  the operating system code
-     * @param kb  whether to normalize to kilobytes
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     * <pre>
+     * FileSystemUtils.freeSpace("C:"); // Windows
+     * FileSystemUtils.freeSpace("/volume"); // *nix
+     * </pre>
+     *
+     * The free space is calculated via the command line. It uses 'dir /-c' on Windows and 'df' on *nix.
+     *
+     * @param path    the path to get free space for, not null, not empty on Unix
+     * @param os      the operating system code
+     * @param kb      whether to normalize to kilobytes
+     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the amount of free drive space on the drive or volume
      * @throws IllegalArgumentException if the path is invalid
-     * @throws IllegalStateException if an error occurred in initialization
-     * @throws IOException if an error occurs when finding the free space
+     * @throws IllegalStateException    if an error occurred in initialization
+     * @throws IOException              if an error occurs when finding the free space
      */
     long freeSpaceOS(final String path, final int os, final boolean kb, final Duration timeout) throws IOException {
         Objects.requireNonNull(path, "path");
@@ -289,16 +269,14 @@
     /**
      * Find free space on the *nix platform using the 'df' command.
      *
-     * @param path  the path to get free space for
-     * @param kb  whether to normalize to kilobytes
-     * @param posix  whether to use the POSIX standard format flag
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     * @param path    the path to get free space for
+     * @param kb      whether to normalize to kilobytes
+     * @param posix   whether to use the POSIX standard format flag
+     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the amount of free drive space on the volume
      * @throws IOException If an I/O error occurs
      */
-    long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout)
-            throws IOException {
+    long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout) throws IOException {
         if (path.isEmpty()) {
             throw new IllegalArgumentException("Path must not be empty");
         }
@@ -342,23 +320,29 @@
     /**
      * Find free space on the Windows platform using the 'dir' command.
      *
-     * @param path  the path to get free space for, including the colon
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     * @param pathStr    the path to get free space for, including the colon
+     * @param timeout The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the amount of free drive space on the drive
      * @throws IOException If an I/O error occurs
      */
-    long freeSpaceWindows(final String path, final Duration timeout) throws IOException {
-        String normPath = FilenameUtils.normalize(path, false);
-        if (normPath == null) {
-            throw new IllegalArgumentException(path);
-        }
-        if (!normPath.isEmpty() && normPath.charAt(0) != '"') {
-            normPath = "\"" + normPath + "\"";
+    long freeSpaceWindows(final String pathStr, final Duration timeout) throws IOException {
+        Objects.requireNonNull(pathStr, "path");
+        final String trimPathStr = pathStr.trim();
+        final Path normPath;
+        if (!trimPathStr.isEmpty()) {
+            if (trimPathStr.charAt(0) != '"') {
+                // Paths.get throws IAE if the path is bad before we pass it to a shell.
+                normPath = Paths.get(trimPathStr).normalize();
+            } else {
+                // Paths.get throws IAE if the path is bad before we pass it to a shell.
+                normPath = Paths.get(trimPathStr.substring(1, trimPathStr.length() - 1)).normalize();
+            }
+        } else {
+            normPath = Paths.get(trimPathStr).normalize();
         }
 
         // build and run the 'dir' command
-        final String[] cmdAttribs = { "cmd.exe", "/C", "dir /a /-c " + normPath };
+        final String[] cmdAttribs = { "cmd.exe", "/C", "dir /a /-c \"" + normPath + "\""};
 
         // read in the output of the command to an ArrayList
         final List<String> lines = performCommand(cmdAttribs, Integer.MAX_VALUE, timeout);
@@ -370,17 +354,17 @@
         for (int i = lines.size() - 1; i >= 0; i--) {
             final String line = lines.get(i);
             if (!line.isEmpty()) {
-                return parseDir(line, normPath);
+                return parseDir(line, trimPathStr);
             }
         }
         // all lines are blank
-        throw new IOException("Command line 'dir /-c' did not return any info for path '" + normPath + "'");
+        throw new IOException("Command line 'dir /-c' did not return any info for path '" + trimPathStr + "'");
     }
 
     /**
      * Opens the process to the operating system.
      *
-     * @param cmdAttribs  the command line parameters
+     * @param cmdAttribs the command line parameters
      * @return the process
      * @throws IOException If an I/O error occurs
      */
@@ -391,8 +375,8 @@
     /**
      * Parses the bytes from a string.
      *
-     * @param freeSpace  the free space string
-     * @param path  the path
+     * @param freeSpace the free space string
+     * @param path      the path
      * @return the number of bytes
      * @throws IOException If an I/O error occurs
      */
@@ -412,8 +396,8 @@
     /**
      * Parses the Windows dir response last line
      *
-     * @param line  the line to parse
-     * @param path  the path that was sent
+     * @param line the line to parse
+     * @param path the path that was sent
      * @return the number of bytes
      * @throws IOException If an I/O error occurs
      */
@@ -462,10 +446,9 @@
     /**
      * Performs an OS command.
      *
-     * @param cmdAttribs  the command line parameters
-     * @param max The maximum limit for the lines returned
-     * @param timeout The timeout amount in milliseconds or no timeout if the value
-     *  is zero or less
+     * @param cmdAttribs the command line parameters
+     * @param max        The maximum limit for the lines returned
+     * @param timeout    The timeout amount in milliseconds or no timeout if the value is zero or less
      * @return the lines returned by the command, converted to lower-case
      * @throws IOException if an error occurs
      */
diff --git a/src/test/java/org/apache/commons/io/FileSystemUtilsTest.java b/src/test/java/org/apache/commons/io/FileSystemUtilsTest.java
index 617f03c..b6af69a 100644
--- a/src/test/java/org/apache/commons/io/FileSystemUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/FileSystemUtilsTest.java
@@ -31,6 +31,8 @@
 import java.util.Locale;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * Tests {@link FileSystemUtils}.
@@ -93,6 +95,10 @@
 
     private static final Duration NEG_1_TIMEOUT = Duration.ofMillis(-1);
 
+    static char[] getIllegalFileNameChars() {
+        return FileSystem.getCurrent().getIllegalFileNameChars();
+    }
+
     @Test
     public void testGetFreeSpace_String() throws Exception {
         // test coverage, as we can't check value
@@ -332,6 +338,17 @@
         assertEquals(189416L, fsu.freeSpaceUnix("/", false, false, NEG_1_TIMEOUT));
     }
 
+    @ParameterizedTest
+    @MethodSource("getIllegalFileNameChars")
+    public void testGetFreeSpaceWindows_IllegalFileName(final char illegalFileNameChar) throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new FileSystemUtils().freeSpaceWindows("\\ \"" + illegalFileNameChar, NEG_1_TIMEOUT));
+    }
+
+    @Test
+    public void testGetFreeSpaceWindows_IllegalFileNames() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new FileSystemUtils().freeSpaceWindows("\\ \"", NEG_1_TIMEOUT));
+    }
+
     @Test
     public void testGetFreeSpaceWindows_String_EmptyMultiLineResponse() {
         final String lines = "\n\n";
@@ -353,7 +370,7 @@
                         "17/08/2005  21:44    <DIR>          Desktop\n" +
                         "               7 File(s)         180260 bytes\n" +
                         "              10 Dir(s)     41411551232 bytes free";
-        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c ");
+        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c \"\"");
         assertEquals(41411551232L, fsu.freeSpaceWindows("", NEG_1_TIMEOUT));
     }
 
@@ -370,7 +387,6 @@
         final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
         assertThrows(IOException.class, () -> fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
     }
-
     @Test
     public void testGetFreeSpaceWindows_String_NormalResponse() throws Exception {
         final String lines =
@@ -388,6 +404,7 @@
         final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c \"C:\"");
         assertEquals(41411551232L, fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
     }
+
     @Test
     public void testGetFreeSpaceWindows_String_NoSuchDirectoryResponse() {
         final String lines =