diff --git a/System.IO.FileSystem.UnitTests/FileUnitTests.cs b/System.IO.FileSystem.UnitTests/FileUnitTests.cs
index ac0c3ac..7992d5a 100644
--- a/System.IO.FileSystem.UnitTests/FileUnitTests.cs
+++ b/System.IO.FileSystem.UnitTests/FileUnitTests.cs
@@ -9,7 +9,7 @@ public class FileUnitTests
[Setup]
public void Setup()
{
- //Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware.");
+ Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware.");
}
private const string Root = @"I:\";
diff --git a/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs b/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs
new file mode 100644
index 0000000..e7fda43
--- /dev/null
+++ b/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs
@@ -0,0 +1,19 @@
+using nanoFramework.TestFramework;
+
+namespace System.IO.FileSystem.UnitTests
+{
+ [TestClass]
+ public class PathInternalUnitTests
+ {
+ [TestMethod]
+ public void IsValidDriveChar_returns_true()
+ {
+ var tests = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
+
+ foreach (var test in tests)
+ {
+ Assert.IsTrue(PathInternal.IsValidDriveChar(test), $"Case: {test}");
+ }
+ }
+ }
+}
diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs
new file mode 100644
index 0000000..903e32d
--- /dev/null
+++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs
@@ -0,0 +1,404 @@
+using nanoFramework.TestFramework;
+
+namespace System.IO.FileSystem.UnitTests
+{
+ [TestClass]
+ internal class PathUnitTests
+ {
+ [TestMethod]
+ public void ChangeExtension_adds_extension()
+ {
+ const string path = @"I:\file";
+ const string expect = @"I:\file.new";
+
+ Assert.AreEqual(expect, Path.ChangeExtension(path, "new"));
+ Assert.AreEqual(expect, Path.ChangeExtension(path, ".new"));
+ }
+
+ [TestMethod]
+ public void ChangeExtension_changes_extension()
+ {
+ const string path = @"I:\file.old";
+ const string expect = @"I:\file.new";
+
+ Assert.AreEqual(expect, Path.ChangeExtension(path, "new"));
+ Assert.AreEqual(expect, Path.ChangeExtension(path, ".new"));
+ }
+
+ [TestMethod]
+ public void ChangeExtension_removes_extension()
+ {
+ const string path = @"I:\file.old";
+ const string expect = @"I:\file";
+
+ Assert.AreEqual(expect, Path.ChangeExtension(path, null));
+ }
+
+ [TestMethod]
+ public void ChangeExtension_returns_empty_string_if_path_is_empty_string()
+ {
+ Assert.AreEqual(string.Empty, Path.ChangeExtension(string.Empty, ".new"));
+ }
+
+ [TestMethod]
+ public void ChangeExtension_returns_null_if_path_is_null()
+ {
+ Assert.IsNull(Path.ChangeExtension(null, ".new"));
+ }
+
+ [TestMethod]
+ public void Combine_returns_path1_if_path2_is_empty_string()
+ {
+ var path1 = "path1";
+ var path2 = string.Empty;
+
+ var actual = Path.Combine(path1, path2);
+
+ Assert.AreEqual(path1, actual);
+ }
+
+ [TestMethod]
+ public void Combine_combines_paths()
+ {
+ var expect = @"I:\Path1\Path2\File.ext";
+
+ Assert.AreEqual(expect, Path.Combine(@"I:\Path1", @"Path2\File.ext"));
+ Assert.AreEqual(expect, Path.Combine(@"I:\Path1\", @"Path2\File.ext"));
+ }
+
+ [TestMethod]
+ public void Combine_returns_path2_if_it_is_an_absolute_path()
+ {
+ var path1 = @"I:\Directory";
+ var path2 = @"I:\Absolute\Path";
+
+ var actual = Path.Combine(path1, path2);
+
+ Assert.AreEqual(path2, actual);
+ }
+
+ [TestMethod]
+ public void Combine_returns_path2_if_path1_is_empty_string()
+ {
+ var path1 = string.Empty;
+ var path2 = "path2";
+
+ var actual = Path.Combine(path1, path2);
+
+ Assert.AreEqual(path2, actual);
+ }
+
+ [TestMethod]
+ public void Combine_throws_if_path1_is_null()
+ {
+ Assert.ThrowsException(typeof(ArgumentNullException), () => { Path.Combine(null, "File.ext"); });
+ }
+
+ [TestMethod]
+ public void Combine_throws_if_path2_is_null()
+ {
+ Assert.ThrowsException(typeof(ArgumentNullException), () => { Path.Combine(@"I:\Directory", null); });
+ }
+
+ [TestMethod]
+ public void GetDirectoryName_returns_directory()
+ {
+ var tests = new[] { @"I:\directory", @"I:\directory\", @"I:\directory\file.ext" };
+ var answers = new[] { @"I:\", @"I:\directory", @"I:\directory" };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ var expected = answers[i];
+
+ Assert.AreEqual(expected, Path.GetDirectoryName(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void GetDirectoryName_returns_directory_UNC_paths()
+ {
+ var tests = new[] { @"\\server\share\", @"\\server\share\file.ext" };
+ var answers = new[] { @"\\server\share", @"\\server\share" };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ var expected = answers[i];
+
+ Assert.AreEqual(expected, Path.GetDirectoryName(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void GetDirectoryName_returns_null()
+ {
+ Assert.IsNull(Path.GetDirectoryName(null), $"Case: 'null'");
+
+ // TODO: Would like to add '(string) null' to these cases but for some reason this crashes. Investigate further and open defect
+ var tests = new[] { string.Empty, " ", @"\", "C:", @"C:\" };
+ foreach (var test in tests)
+ {
+ var actual = Path.GetDirectoryName(test);
+ var message = $"Actual: '{actual}'. Case: '{test}'";
+
+ Assert.IsNull(Path.GetDirectoryName(test), message);
+ }
+ }
+
+ [TestMethod]
+ public void GetDirectoryName_returns_null_UNC_paths()
+ {
+ Assert.SkipTest("UNC paths are not supported in the default build");
+
+ var tests = new[] { @"\\server\share" };
+ foreach (var test in tests)
+ {
+ var actual = Path.GetDirectoryName(test);
+ var message = $"Actual: '{actual}'. Case: '{test}'";
+
+ Assert.IsNull(Path.GetDirectoryName(test), message);
+ }
+ }
+ [TestMethod]
+ public void GetExtension_returns_empty_string()
+ {
+ Assert.AreEqual(string.Empty, Path.GetExtension(string.Empty));
+ Assert.AreEqual(string.Empty, Path.GetExtension("file"));
+ Assert.AreEqual(string.Empty, Path.GetExtension("file."));
+ }
+
+ [TestMethod]
+ public void GetExtension_returns_extension()
+ {
+ var file = "file.ext";
+ var expect = ".ext";
+
+ Assert.AreEqual(expect, Path.GetExtension(file));
+ Assert.AreEqual(expect, Path.GetExtension($"I:{file}"));
+ Assert.AreEqual(expect, Path.GetExtension(@$"I:\{file}"));
+ Assert.AreEqual(expect, Path.GetExtension(@$"I:\directory\{file}"));
+ Assert.AreEqual(expect, Path.GetExtension(@$"\{file}"));
+ }
+
+ [TestMethod]
+ public void GetExtension_returns_extension_UNC_paths()
+ {
+ var file = "file.ext";
+ var expect = ".ext";
+
+ Assert.AreEqual(expect, Path.GetExtension(@$"\\server\share\{file}"));
+ Assert.AreEqual(expect, Path.GetExtension(@$"\\server\share\directory\{file}"));
+ }
+
+ [TestMethod]
+ public void GetExtension_returns_null()
+ {
+ Assert.IsNull(Path.GetExtension(null));
+ }
+
+ [TestMethod]
+ public void GetFilename_returns_empty_string()
+ {
+ Assert.AreEqual(string.Empty, Path.GetFileName("I:"));
+ Assert.AreEqual(string.Empty, Path.GetFileName(@"I:\"));
+ }
+
+ [TestMethod]
+ public void GetFilename_returns_filename_without_extension()
+ {
+ Assert.AreEqual("file", Path.GetFileName(@"I:\directory\file"));
+ Assert.AreEqual("file.ext", Path.GetFileName(@"I:\directory\file.ext"));
+ Assert.AreEqual("file", Path.GetFileName(@"I:\file"));
+ Assert.AreEqual("file.ext", Path.GetFileName(@"I:\file.ext"));
+ }
+
+ [TestMethod]
+ public void GetFilename_returns_filename_without_extension_UNC_paths()
+ {
+ Assert.AreEqual("file", Path.GetFileName(@"\\server\share\directory\file"));
+ Assert.AreEqual("file.ext", Path.GetFileName(@"\\server\share\directory\file.ext"));
+ }
+
+ [TestMethod]
+ public void GetFilename_returns_null()
+ {
+ Assert.IsNull(Path.GetFileName(null));
+ }
+
+ [TestMethod]
+ public void GetFilenameWithoutExtension_returns_empty_string()
+ {
+ Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension("I:"));
+ Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension(@"I:\"));
+ }
+
+ [TestMethod]
+ public void GetFilenameWithoutExtension_returns_filename_without_extension()
+ {
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\directory\file"));
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\directory\file.ext"));
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\file"));
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\file.ext"));
+ }
+
+ [TestMethod]
+ public void GetFilenameWithoutExtension_returns_filename_without_extension_UNC_paths()
+ {
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file"));
+ Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file.ext"));
+ }
+
+ [TestMethod]
+ public void GetFilenameWithoutExtension_returns_null()
+ {
+ Assert.IsNull(Path.GetFileNameWithoutExtension(null));
+ }
+
+ [TestMethod]
+ public void GetPathRoot_returns_empty_string()
+ {
+ Assert.AreEqual(string.Empty, Path.GetPathRoot(@"directory\file"));
+ Assert.AreEqual(string.Empty, Path.GetPathRoot(@"directory\file.ext"));
+ Assert.AreEqual(string.Empty, Path.GetPathRoot("file"));
+ Assert.AreEqual(string.Empty, Path.GetPathRoot("file.ext"));
+ }
+
+ [TestMethod]
+ public void GetPathRoot_returns_null()
+ {
+ Assert.IsNull(Path.GetPathRoot(null));
+ Assert.IsNull(Path.GetPathRoot(" "));
+ }
+
+ [TestMethod]
+ public void GetPathRoot_returns_root()
+ {
+ var tests = new[]
+ {
+ "I:", @"I:\directory\file", @"I:\directory\file.ext", @"I:\file", @"I:\file.ext"
+ };
+
+ var answers = new[] { "I:", @"I:\", @"I:\", @"I:\", @"I:\" };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ var expected = answers[i];
+
+ Assert.AreEqual(expected, Path.GetPathRoot(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void GetPathRoot_returns_root_UNC_paths()
+ {
+ Assert.SkipTest("UNC paths are not supported in the default build");
+
+ var tests = new[]
+ {
+ @"\\server\share\directory\file", @"\\server\share\directory\file.ext"
+ };
+
+ var answers = new[] { @"\\server\share", @"\\server\share" };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ var expected = answers[i];
+
+ Assert.AreEqual(expected, Path.GetPathRoot(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void HasExtension_returns_false()
+ {
+ var tests = new[]
+ {
+ "file", @"\file.", @"\", "/", "I:", @"I:\", @"I:\directory\"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsFalse(Path.HasExtension(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void HasExtension_returns_false_UNC_paths()
+ {
+ var tests = new[]
+ {
+ @"\\server\share\file.", @"\\server\share\directory\file"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsFalse(Path.HasExtension(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void HasExtension_returns_true()
+ {
+ var tests = new[]
+ {
+ "file.ext", @"\file.ext", "/file.ext", "I:file.ext", @"I:\file.ext", @"I:\directory\file.ext"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsTrue(Path.HasExtension(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void HasExtension_returns_true_UNC_paths()
+ {
+ var tests = new[]
+ {
+ @"\\server\share\file.ext", @"\\server\share\directory\file.ext"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsTrue(Path.HasExtension(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void IsPathRooted_returns_true()
+ {
+ var tests = new[]
+ {
+ @"\", "/", "I:", @"I:\", @"I:\file.ext", @"I:\directory\file.ext"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsTrue(Path.IsPathRooted(test), $"Case: {test}");
+ }
+ }
+
+ [TestMethod]
+ public void IsPathRooted_returns_true_UNC_paths()
+ {
+ var tests = new[]
+ {
+ @"\\server\share", @"\\server\share\file.ext", @"\\server\share\directory\file.ext"
+ };
+
+ for (var i = 0; i < tests.Length; i++)
+ {
+ var test = tests[i];
+ Assert.IsTrue(Path.IsPathRooted(test), $"Case: {test}");
+ }
+ }
+ }
+}
diff --git a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj
index 22e5a91..59d0ce1 100644
--- a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj
+++ b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj
@@ -23,12 +23,23 @@
v1.0
true
+
+ true
+
+
+ key.snk
+
+
+ false
+
$(MSBuildProjectDirectory)\nano.runsettings
+
+
diff --git a/System.IO.FileSystem.UnitTests/key.snk b/System.IO.FileSystem.UnitTests/key.snk
new file mode 100644
index 0000000..67c9bb0
Binary files /dev/null and b/System.IO.FileSystem.UnitTests/key.snk differ
diff --git a/System.IO.FileSystem.UnitTests/nano.runsettings b/System.IO.FileSystem.UnitTests/nano.runsettings
index 93ce85e..438c21c 100644
--- a/System.IO.FileSystem.UnitTests/nano.runsettings
+++ b/System.IO.FileSystem.UnitTests/nano.runsettings
@@ -9,7 +9,7 @@
None
- False
+ True
COM3
diff --git a/System.IO.FileSystem/File.cs b/System.IO.FileSystem/File.cs
index e9a8a28..b2ba13a 100644
--- a/System.IO.FileSystem/File.cs
+++ b/System.IO.FileSystem/File.cs
@@ -85,7 +85,6 @@ public static FileStream Create(string path)
///
/// The name of the file to be deleted. Wildcard characters are not supported.
/// is or empty.
-
/// Directory is not found or is read-only or a directory.
public static void Delete(string path)
{
@@ -94,8 +93,6 @@ public static void Delete(string path)
throw new ArgumentException();
}
- Path.CheckInvalidPathChars(path);
-
try
{
byte attributes;
diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs
index ceefc82..d04c4ba 100644
--- a/System.IO.FileSystem/Path.cs
+++ b/System.IO.FileSystem/Path.cs
@@ -3,261 +3,218 @@
// See LICENSE file in the project root for full license information.
//
-using System.Collections;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
namespace System.IO
{
///
- /// Performs operations on String instances that contain file or directory path information.
+ /// Provides methods for processing file system strings in a cross-platform manner.
+ /// Most of the methods don't do a complete parsing (such as examining a UNC hostname),
+ /// but they will handle most string operations.
///
- public sealed class Path
+ public static class Path
{
- #region Constants
-
- // From FS_decl.h
- private const int FSMaxPathLength = 260 - 2;
- private const int FSMaxFilenameLength = 256;
- private const int FSNameMaxLength = 7 + 1;
-
- // Windows API definitions
- internal const int MAX_PATH = 260; // From WinDef.h
-
- #endregion
-
-
- #region Variables
+ // Public static readonly variant of the separators. The Path implementation itself is using
+ // internal const variant of the separators for better performance.
///
/// Provides a platform-specific character used to separate directory levels in a path string that reflects a hierarchical file system organization.
///
- public static readonly char DirectorySeparatorChar = '\\';
+ public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar;
///
- /// Provides a platform-specific array of characters that cannot be specified in path string arguments passed to members of the Path class.
+ /// Provides a platform-specific alternate character used to separate directory levels in a path string that reflects a hierarchical file system organization.
///
- public static readonly char[] InvalidPathChars = { '/', '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31 };
-
- internal static readonly char[] m_illegalCharacters = { '?', '*' };
-
- #endregion
-
-
- #region Constructor
+ public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar;
- private Path()
- {
- }
-
- #endregion
+ ///
+ /// Provides a platform-specific volume separator character.
+ ///
+ public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar;
+ ///
+ /// A platform-specific separator character used to separate path strings in environment variables.
+ ///
+ public static readonly char PathSeparator = PathInternal.PathSeparator;
- #region Methods
+ // TODO: This is not needed when has an overload for
+ private const string ExtensionSeparatorString = ".";
///
- /// Changes the extension of a file path. The path
parameter
- /// specifies a file path, and the extension
parameter
- /// specifies a file extension (with a leading period, such as
- /// ".exe"
or".cool"
).
- ///
- /// The function returns a file path with the same root, directory, and base
- /// name parts as path
, but with the file extension changed to
- /// the specified extension.Ifpath
is null, the function
- /// returns null. If path
does not contain a file extension,
- /// the new file extension is appended to the path.Ifextension
- /// is null, any existing extension is removed from path
.
+ /// Changes the extension of a path string.
///
- /// The path for which to change file extension.
- /// The new file extension (with a leading period), or null to remove the extension.
- ///
- public static string ChangeExtension(
- string path,
- string extension)
+ /// The path information to modify.
+ ///
+ /// The new extension (with or without a leading period). Specify to remove an existing extension from .
+ ///
+ ///
+ /// The modified path information.
+ ///
+ /// If is or an empty string (""), the path information is returned unmodified.
+ /// If is , the returned string contains the specified path with its extension removed.
+ /// If has no extension, and is not , the returned path string
+ /// contains appended to the end of .
+ ///
+ public static string ChangeExtension(string path, string extension)
{
- if (path != null)
+ if (path is null)
{
- CheckInvalidPathChars(path);
-
- string s = path;
+ return null;
+ }
- for (int i = path.Length; --i >= 0;)
- {
- char ch = path[i];
+ var subLength = path.Length;
+ if (subLength == 0)
+ {
+ return string.Empty;
+ }
- if (ch == '.')
- {
- s = path.Substring(0, i);
- break;
- }
+ for (var i = path.Length - 1; i >= 0; i--)
+ {
+ var ch = path[i];
- if (ch == DirectorySeparatorChar) break;
+ if (ch == '.')
+ {
+ subLength = i;
+ break;
}
- if (extension != null && path.Length != 0)
+ if (PathInternal.IsDirectorySeparator(ch))
{
- if (extension.Length == 0 || extension[0] != '.')
- {
- s += ".";
- }
-
- s += extension;
+ break;
}
+ }
- return s;
+ if (extension is null)
+ {
+ return path.Substring(0, subLength);
}
- return null;
+ var subPath = path.Substring(0, subLength);
+
+ return extension.StartsWith(ExtensionSeparatorString) ?
+ string.Concat(subPath, extension) :
+ string.Concat(subPath, ExtensionSeparatorString, extension);
}
///
- /// Returns the directory path of a file path. This method effectively
- /// removes the last element of the given file path, i.e.it returns a
- /// string consisting of all characters up to but not including the last
- /// backslash("\") in the file path. The returned value is null if the file
- /// path is null or if the file path denotes a root (such as "\", "C:", or
- /// "\\server\share").
+ /// Combines two strings into a path.
///
- /// The path of a file or directory.
- /// The directory path of the given path, or null if the given path denotes a root.
- public static string GetDirectoryName(string path)
+ /// The first path to combine.
+ /// The second path to combine.
+ ///
+ /// The combined paths. If one of the specified paths is a zero-length string, this method returns the other path.
+ /// If contains an absolute path, this method returns .
+ ///
+ /// or is .
+ public static string Combine(string path1, string path2)
{
- if (path != null)
+ if (path1 is null || path2 is null)
{
- NormalizePath(path, false);
-
- int root = GetRootLength(path);
-
- int i = path.Length;
-
- if (i > root)
- {
- i = path.Length;
-
- if (i == root)
- {
- return null;
- }
-
- var lastPathPostion = path.LastIndexOf(DirectorySeparatorChar);
-
- if (lastPathPostion == -1)
- {
- return string.Empty;
- }
-
- return path.Substring(0, lastPathPostion);
- }
+ throw new ArgumentNullException();
}
- return null;
+ return CombineInternal(path1, path2);
}
- ///
- /// Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers
- /// are specified for the first part of the DirectoryInfo name.
- ///
- ///
- ///
- internal static int GetRootLength(string path)
+ private static string CombineInternal(string first, string second)
{
- CheckInvalidPathChars(path);
-
- int i = 0;
- int length = path.Length;
-
- if (length >= 1
- && IsDirectorySeparator(path[0]))
+ if (string.IsNullOrEmpty(first))
{
- // Handles UNC names and directories off current drive's root.
- i = 1;
-
- if (length >= 2
- && IsDirectorySeparator(path[1]))
- {
- i = 2;
- int n = 2;
+ return second;
+ }
- while (i < length && (path[i] != DirectorySeparatorChar || --n > 0))
- {
- i++;
- }
- }
+ if (string.IsNullOrEmpty(second))
+ {
+ return first;
}
- return i;
- }
+ if (IsPathRooted(second))
+ {
+ return second;
+ }
- internal static bool IsDirectorySeparator(char c)
- {
- return c == DirectorySeparatorChar;
+ return JoinInternal(first, second);
}
///
- /// Gets an array containing the characters that are not allowed in path names.
+ /// Returns the directory portion of a file path. This method effectively
+ /// removes the last segment of the given file path, i.e. it returns a
+ /// string consisting of all characters up to but not including the last
+ /// backslash ("\") in the file path. The returned value is null if the
+ /// specified path is null, empty, or a root (such as "\", "C:", or
+ /// "\\server\share").
///
- /// An array containing the characters that are not allowed in path names.
- public static char[] GetInvalidPathChars()
+ ///
+ /// Directory separators are normalized in the returned string.
+ ///
+ public static string GetDirectoryName(string path)
{
- return (char[])InvalidPathChars.Clone();
+ if (path is null || PathInternal.IsEffectivelyEmpty(path))
+ {
+ return null;
+ }
+
+ var end = GetDirectoryNameOffset(path);
+ return end >= 0 ? PathInternal.NormalizeDirectorySeparators(path.Substring(0, end)) : null;
}
- ///
- /// Returns the absolute path for the specified path string.
- ///
- /// The file or directory for which to obtain absolute path information.
- ///
- public static string GetFullPath(string path)
+ internal static int GetDirectoryNameOffset(string path)
{
- /*
- ValidateNullOrEmpty(path);
+ var rootLength = PathInternal.GetRootLength(path);
+ var end = path.Length;
+
+ if (end <= rootLength)
+ {
+ return -1;
+ }
+
+ while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end]))
+ {
+ }
- if (!Path.IsPathRooted(path))
+ // Trim off any remaining separators (to deal with C:\foo\\bar)
+ while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1]))
{
- string currDir = Directory.GetCurrentDirectory();
- path = Path.Combine(currDir, path);
+ end--;
}
- return NormalizePath(path, false);
- */
- throw new NotImplementedException();
+ return end;
}
///
- /// Returns the extension of the given path. The returned value includes the
- /// period(".") character of the extension except when you have a terminal period when you get String.Empty, such as ".exe"
or
- /// ".cpp"
. The returned value is null if the given path is
- /// null or if the given path does not include an extension.
+ /// Returns the extension (including the period ".") of the specified path string.
///
- /// The path of a file or directory.
- /// The extension of the given path, or null if the given path does not include an extension.
- /// if path contains invalid characters.
+ /// The path string from which to get the extension.
+ ///
+ /// The extension of the specified path (including the period "."), or , or .
+ /// If is , returns .
+ /// If path does not have extension information, returns .
+ [return: NotNullIfNotNull("path")]
public static string GetExtension(string path)
{
- if (path == null)
+ if (path is null)
{
return null;
}
- CheckInvalidPathChars(path);
+ var length = path.Length;
- int length = path.Length;
-
- for (int i = length; --i >= 0;)
+ for (var i = length - 1; i >= 0; i--)
{
- char ch = path[i];
-
+ var ch = path[i];
+
if (ch == '.')
{
if (i != length - 1)
{
return path.Substring(i, length - i);
}
- else
- {
- return string.Empty;
- }
+
+ return string.Empty;
}
- if (ch == DirectorySeparatorChar)
+ if (PathInternal.IsDirectorySeparator(ch))
{
break;
}
@@ -267,32 +224,32 @@ public static string GetExtension(string path)
}
///
- /// Returns the name and extension parts of the given path. The resulting
- /// string contains the characters of path
that follow the last
- /// backslash ("\"), slash ("/"), or colon (":") character in
- /// path
.The resulting string is the entire path if path
- /// contains no backslash after removing trailing slashes, slash, or colon characters.The resulting
- /// string is null if path
is null.
+ /// Returns the file name and extension of the specified path string.
///
- /// The path of a file or directory.
- /// The name and extension parts of the given path.
- /// if path contains invalid characters.
+ /// The path string from which to obtain the file name and extension.
+ ///
+ /// The characters after the last directory separator character in .
+ /// If the last character of is a directory or volume separator character, this method returns .
+ /// If is , this method returns .
+ ///
+ [return: NotNullIfNotNull("path")]
public static string GetFileName(string path)
{
- if (path != null)
+ if (path is null)
{
- CheckInvalidPathChars(path);
+ return null;
+ }
- int length = path.Length;
+ var root = GetPathRoot(path).Length;
- for (int i = length; --i >= 0;)
- {
- char ch = path[i];
+ // We don't want to cut off "C:\file.txt:stream" (i.e. should be "file.txt:stream")
+ // but we *do* want "C:Foo" => "Foo". This necessitates checking for the root.
- if (ch == DirectorySeparatorChar)
- {
- return path.Substring(i + 1, length - i - 1);
- }
+ for (var i = path.Length; --i >= 0;)
+ {
+ if (i < root || PathInternal.IsDirectorySeparator(path[i]))
+ {
+ return path.Substring(i + 1);
}
}
@@ -303,372 +260,137 @@ public static string GetFileName(string path)
/// Returns the file name of the specified path string without the extension.
///
/// The path of the file.
- ///
+ /// The string returned by , minus the last period (.) and all characters following it.
+ [return: NotNullIfNotNull("path")]
public static string GetFileNameWithoutExtension(string path)
{
- path = GetFileName(path);
-
- if (path != null)
+ if (path is null)
{
- int i;
-
- if ((i = path.LastIndexOf('.')) == -1)
- {
- // No path extension found
- return path;
- }
- else
- {
- return path.Substring(0, i);
- }
+ return null;
}
- return null;
- }
+ var fileName = GetFileName(path);
+ var lastPeriod = fileName.LastIndexOf('.');
- ///
- /// Tests if a path includes a file extension. The result is
- /// true
if the characters that follow the last directory
- /// separator('\\' or '/') or volume separator(':') in the path include
- /// a period(".") other than a terminal period.The result is false
otherwise.
- ///
- /// The path of a file or directory.
- /// The root portion of the given path.
- /// if path contains invalid characters.
- public static string GetPathRoot(string path)
- {
- return path == null ? null : path.Substring(0, path.IndexOf(DirectorySeparatorChar));
+ return lastPeriod < 0 ?
+ fileName : // No extension was found
+ fileName.Substring(0, lastPeriod);
}
///
- /// Tests if a path includes a file extension. The result is
- /// true
if the characters that follow the last directory
- /// separator('\\' or '/') or volume separator(':') in the path include
- /// a period(".") other than a terminal period.The result is false
otherwise.
+ /// Gets an array containing the characters that are not allowed in file names.
///
- /// The path to test.
- /// Boolean indicating whether the path includes a file extension.
- /// if path contains invalid characters.
- public static bool HasExtension(string path)
+ /// An array containing the characters that are not allowed in file names.
+ public static char[] GetInvalidFileNameChars() => new char[]
{
- if (path != null)
- {
- CheckInvalidPathChars(path);
-
- for (int i = path.Length; --i >= 0;)
- {
- char ch = path[i];
-
- if (ch == '.')
- {
- return i != path.Length - 1;
- }
-
- if (ch == DirectorySeparatorChar)
- {
- break;
- }
- }
- }
-
- return false;
- }
+ '\"', '<', '>', '|', '\0',
+ (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
+ (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
+ (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
+ (char)31, ':', '*', '?', '\\', '/'
+ };
///
- /// Tests if the given path contains a root. A path is considered rooted
- /// if it starts with a backslash("\") or a drive letter and a colon (":").
+ /// Gets an array containing the characters that are not allowed in path names.
///
- /// The path to test.
- /// Boolean indicating whether the path is rooted.
- /// if path contains invalid characters.
- public static bool IsPathRooted(string path)
+ /// An array containing the characters that are not allowed in path names.
+ public static char[] GetInvalidPathChars() => new[]
{
- if (path != null)
- {
- CheckInvalidPathChars(path);
-
- int length = path.Length;
-
- if (length >= 1 && (path[0] == DirectorySeparatorChar))
- {
- return true;
- }
- }
-
- return false;
- }
+ '|', '\0',
+ (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
+ (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
+ (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
+ (char)31
+ };
///
- /// Combines two strings into a path.
+ /// Gets the root directory information from the path contained in the specified string.
///
- /// The first path to combine.
- /// The second path to combine.
- ///
- public static string Combine(string path1, string path2)
- {
- if (path1 == null || path2 == null)
- {
-#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one
- throw new ArgumentNullException(/*(path1==null) ? "path1" : "path2"*/);
-#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one
- }
-
- CheckInvalidPathChars(path1);
- CheckInvalidPathChars(path2);
-
- if (path2.Length == 0)
- {
- return path1;
- }
-
- if (path1.Length == 0)
- {
- return path2;
- }
-
- if (IsPathRooted(path2))
- {
- return path2;
- }
-
- char ch = path1[path1.Length - 1];
-
- return ch != DirectorySeparatorChar ? path1 + DirectorySeparatorChar + path2 : path1 + path2;
- }
-
- //--//
-
- internal static void CheckInvalidPathChars(string path)
+ /// A string containing the path from which to obtain root directory information.
+ ///
+ /// The root directory of if it is rooted.
+ ///
+ /// -or-
+ ///
+ /// if does not contain root directory information.
+ ///
+ /// -or-
+ ///
+ /// if is or is effectively empty.
+ ///
+ public static string GetPathRoot(string path)
{
- if (-1 != path.IndexOfAny(InvalidPathChars))
+ if (PathInternal.IsEffectivelyEmpty(path))
{
-#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one
- throw new ArgumentException(/*Environment.GetResourceString("Argument_InvalidPathChars")*/);
-#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one
+ return null;
}
- }
+ var pathRootLength = PathInternal.GetRootLength(path);
+ var pathRoot = pathRootLength <= 0 ? string.Empty : path.Substring(0, pathRootLength);
- internal static void ValidateNullOrEmpty(string str)
- {
- if (str == null)
- {
- throw new ArgumentNullException("Path is null.");
- }
-
- if (str.Length == 0)
- {
- throw new ArgumentException("Path length is 0.");
- }
+ return PathInternal.NormalizeDirectorySeparators(pathRoot);
}
- internal static string NormalizePath(string path, bool pattern)
+ ///
+ /// Determines whether a path includes a file name extension.
+ ///
+ /// The path to search for an extension.
+ ///
+ /// if the characters that follow the last directory separator (\ or /) or volume separator (:)
+ /// in the path include a period (.) followed by one or more characters; otherwise, .
+ ///
+ public static bool HasExtension([NotNullWhen(true)] string path)
{
- ValidateNullOrEmpty(path);
-
- int pathLength = path.Length;
-
- int i;
-
- for (i = 0; i < pathLength; i++)
- {
- if (path[i] != '\\')
- {
- break;
- }
- }
-
- bool rootedPath = false;
- bool serverPath = false;
-
- // Handle some of the special cases.
- // 1. Root (\)
- // 2. Server (\\server).
- // 3. InvalidPath (\\\, \\\\, etc).
- if (i == 1)
- {
- rootedPath = true;
- }
- else if ((i == 2) && (pathLength > 2))
- {
- serverPath = true;
- }
- else if (i > 2)
- {
- throw new ArgumentException("Path contains 3 and more successive backslashes.");
- }
-
- if (rootedPath)
- {
- int limit = i + FSNameMaxLength;
-
- for (; i < limit && i < pathLength; i++)
- {
- if (path[i] == '\\')
- {
- break;
- }
- }
-
- if (i == limit)
- {
- // if the namespace is too long
- throw new IOException("", (int)IOException.IOExceptionErrorCode.VolumeNotFound);
- }
- else if (pathLength - i >= FSMaxPathLength)
- {
- // if the "relative" path exceeds the limit
- throw new IOException("", (int)IOException.IOExceptionErrorCode.PathTooLong);
- }
- }
- else // For non-rooted paths (i.e. server paths or relative paths), we follow the MAX_PATH (260) limit from desktop
+ if (path is null)
{
- if (pathLength >= MAX_PATH)
- {
- throw new IOException("", (int)IOException.IOExceptionErrorCode.PathTooLong);
- }
+ return false;
}
- string[] pathParts = path.Split(DirectorySeparatorChar);
-
- if (pattern && (pathParts.Length > 1))
- {
- throw new ArgumentException("Path contains only a Directory/FileName");
- }
-
- ArrayList finalPathSegments = new ArrayList();
- int pathPartLen;
-
- for (int e = 0; e < pathParts.Length; e++)
+ for (var i = path.Length - 1; i >= 0; i--)
{
- pathPartLen = pathParts[e].Length;
-
- if (pathPartLen == 0)
- {
- // Do nothing. Apparently paths like c:\\folder\\\file.txt works fine in Windows.
- continue;
- }
- else if (pathPartLen >= FSMaxFilenameLength)
- {
- throw new IOException("", (int)IOException.IOExceptionErrorCode.PathTooLong);
- }
-
- if (pathParts[e].IndexOfAny(InvalidPathChars) != -1)
- {
- throw new ArgumentException("Path contains invalid characters: " + pathParts[e]);
- }
-
- if (!pattern
- && pathParts[e].IndexOfAny(m_illegalCharacters) != -1)
+ var ch = path[i];
+
+ if (ch == '.')
{
- throw new ArgumentException("Path contains illegal characters: " + pathParts[e]);
+ return i != path.Length - 1;
}
- // verify whether pathParts[e] is all '.'s. If it is
- // we have some special cases. Also path with both dots
- // and spaces only are invalid.
- int length = pathParts[e].Length;
- bool spaceFound = false;
-
- for (i = 0; i < length; i++)
+ if (PathInternal.IsDirectorySeparator(ch))
{
- if (pathParts[e][i] == '.')
- {
- continue;
- }
-
- if (pathParts[e][i] == ' ')
- {
- spaceFound = true;
- continue;
- }
-
break;
}
-
- if (i >= length)
- {
- if (!spaceFound)
- {
- // Dots only.
- if (i == 1)
- {
- // Stay in same directory.
- }
- else if (i == 2)
- {
- if (finalPathSegments.Count == 0)
- {
-#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one
- throw new ArgumentException();
-#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one
- }
-
- finalPathSegments.RemoveAt(finalPathSegments.Count - 1);
- }
- else
- {
-#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one
- throw new ArgumentException();
-#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one
- }
- }
- else
- {
- // Just dots and spaces doesn't make the cut.
- throw new ArgumentException("Path only contains dots and spaces.");
- }
- }
- else
- {
- int trim = length - 1;
-
- while (pathParts[e][trim] == ' ' || pathParts[e][trim] == '.')
- {
- trim--;
- }
-
- finalPathSegments.Add(pathParts[e].Substring(0, trim + 1));
- }
}
- string normalizedPath = "";
+ return false;
+ }
- if (rootedPath)
- {
- normalizedPath += @"\";
- }
- else if (serverPath)
+ ///
+ /// Returns a value indicating whether the specified path string contains a root.
+ ///
+ /// The path to test.
+ /// if contains a root; otherwise, .
+ public static bool IsPathRooted([NotNullWhen(true)] string path)
+ {
+ if (path is null)
{
- normalizedPath += @"\\";
-
- // btw, server path must specify server name.
- if (finalPathSegments.Count == 0)
- {
- throw new ArgumentException("Server Path is missing server name.");
- }
+ return false;
}
- bool firstSegment = true;
+ var length = path.Length;
+ return (length >= 1 && PathInternal.IsDirectorySeparator(path[0]))
+ || (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar);
+ }
- for (int e = 0; e < finalPathSegments.Count; e++)
- {
- if (!firstSegment)
- {
- normalizedPath += "\\";
- }
- else
- {
- firstSegment = false;
- }
+ private static string JoinInternal(string first, string second)
+ {
+ Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
- normalizedPath += (string)finalPathSegments[e];
- }
+ var hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+ || PathInternal.IsDirectorySeparator(second[0]);
- return normalizedPath;
+ return hasSeparator ?
+ string.Concat(first, second) :
+ string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second);
}
-
- #endregion
-
}
}
diff --git a/System.IO.FileSystem/PathInternal.cs b/System.IO.FileSystem/PathInternal.cs
new file mode 100644
index 0000000..92df7ed
--- /dev/null
+++ b/System.IO.FileSystem/PathInternal.cs
@@ -0,0 +1,299 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace System.IO
+{
+ /// Contains internal path helpers that are shared between many projects.
+ internal static class PathInternal
+ {
+ internal const char DirectorySeparatorChar = '\\';
+ internal const char AltDirectorySeparatorChar = '/';
+ internal const char VolumeSeparatorChar = ':';
+ internal const char PathSeparator = ';';
+
+ internal const string DirectorySeparatorCharAsString = "\\";
+
+ internal const int MaxShortPath = 260;
+ internal const int MaxShortDirectoryPath = 248;
+
+#if PATH_SUPPORTS_UNC
+ // \\?\, \\.\, \??\
+ internal const int DevicePrefixLength = 4;
+ // \\
+ internal const int UncPrefixLength = 2;
+ // \\?\UNC\, \\.\UNC\
+ internal const int UncExtendedPrefixLength = 8;
+#endif
+
+ ///
+ /// Gets the length of the root of the path (drive, share, etc.).
+ ///
+ internal static int GetRootLength(string path)
+ {
+ var pathLength = path.Length;
+ var i = 0;
+
+#if PATH_SUPPORTS_UNC
+ var deviceSyntax = IsDevice(path);
+ var deviceUnc = deviceSyntax && IsDeviceUNC(path);
+
+ if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0]))
+ {
+ // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
+ if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1])))
+ {
+ // UNC (\\?\UNC\ or \\), scan past server\share
+
+ // Start past the prefix ("\\" or "\\?\UNC\")
+ i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength;
+
+ // Skip two separators at most
+ var n = 2;
+ while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0))
+ {
+ i++;
+ }
+ }
+ else
+ {
+ // Current drive rooted (e.g. "\foo")
+ i = 1;
+ }
+ }
+ else if (deviceSyntax)
+ {
+ // Device path (e.g. "\\?\.", "\\.\")
+ // Skip any characters following the prefix that aren't a separator
+ i = DevicePrefixLength;
+ while (i < pathLength && !IsDirectorySeparator(path[i]))
+ {
+ i++;
+ }
+
+ // If there is another separator take it, as long as we have had at least one
+ // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\")
+ if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i]))
+ {
+ i++;
+ }
+ }
+ else if (pathLength >= 2 && path[1] == VolumeSeparatorChar && IsValidDriveChar(path[0]))
+ {
+ // Valid drive specified path ("C:", "D:", etc.)
+ i = 2;
+
+ // If the colon is followed by a directory separator, move past it (e.g "C:\")
+ if (pathLength > 2 && IsDirectorySeparator(path[2]))
+ {
+ i++;
+ }
+ }
+#else
+ if (pathLength >= 2 && path[1] == VolumeSeparatorChar && IsValidDriveChar(path[0]))
+ {
+ // Valid drive specified path ("C:", "D:", etc.)
+ i = 2;
+
+ // If the colon is followed by a directory separator, move past it (e.g "C:\")
+ if (pathLength > 2 && IsDirectorySeparator(path[2]))
+ {
+ i++;
+ }
+ }
+ else if (pathLength == 1 && IsDirectorySeparator(path[0]))
+ {
+ // Current drive rooted (e.g. "\foo")
+ i = 1;
+ }
+#endif
+
+ return i;
+ }
+
+#if PATH_SUPPORTS_UNC
+ ///
+ /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
+ ///
+ internal static bool IsDevice(string path)
+ {
+ // If the path begins with any two separators is will be recognized and normalized and prepped with
+ // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
+ return IsExtended(path)
+ ||
+ (
+ path.Length >= DevicePrefixLength
+ && IsDirectorySeparator(path[0])
+ && IsDirectorySeparator(path[1])
+ && (path[2] == '.' || path[2] == '?')
+ && IsDirectorySeparator(path[3])
+ );
+ }
+
+ ///
+ /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\)
+ ///
+ internal static bool IsDeviceUNC(string path)
+ {
+ return path.Length >= UncExtendedPrefixLength
+ && IsDevice(path)
+ && IsDirectorySeparator(path[7])
+ && path[4] == 'U'
+ && path[5] == 'N'
+ && path[6] == 'C';
+ }
+#endif
+
+ ///
+ /// True if the given character is a directory separator.
+ ///
+ // [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsDirectorySeparator(char c)
+ {
+ return c is DirectorySeparatorChar or AltDirectorySeparatorChar;
+ }
+
+ ///
+ /// Returns true if the path is effectively empty for the current OS.
+ /// For unix, this is empty or null. For Windows, this is empty, null, or
+ /// just spaces ((char)32).
+ ///
+ internal static bool IsEffectivelyEmpty(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return true;
+ }
+
+ foreach (var c in path.ToCharArray())
+ {
+ if (c != ' ')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+#if PATH_SUPPORTS_UNC
+ ///
+ /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
+ /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
+ /// and path length checks.
+ ///
+ internal static bool IsExtended(string path)
+ {
+ // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
+ // Skipping of normalization will *only* occur if back slashes ('\') are used.
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+#endif
+
+ ///
+ /// Returns true if the given character is a valid drive letter
+ ///
+ internal static bool IsValidDriveChar(char value)
+ {
+ return (uint)((value | 0x20) - 'a') <= (uint)('z' - 'a');
+ }
+
+ ///
+ /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present.
+ /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip).
+ ///
+ /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false.
+ /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as
+ /// such can't be used here (and is overkill for our uses).
+ ///
+ /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments.
+ ///
+ ///
+ /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do
+ /// not need trimming of trailing whitespace here.
+ ///
+ /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization.
+ ///
+ /// For legacy .NET Framework behavior with ExpandShortPaths:
+ /// - It has no impact on GetPathRoot() so doesn't need consideration.
+ /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share).
+ ///
+ /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was
+ /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you
+ /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by
+ /// this undocumented behavior.
+ ///
+ /// We won't match this old behavior because:
+ ///
+ /// 1. It was undocumented
+ /// 2. It was costly (extremely so if it actually contained '~')
+ /// 3. Doesn't play nice with string logic
+ /// 4. Isn't a cross-plat friendly concept/behavior
+ ///
+ [return: NotNullIfNotNull("path")]
+ internal static string NormalizeDirectorySeparators(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ char current;
+
+ // Make a pass to see if we need to normalize so we can potentially skip allocating
+ var normalized = true;
+
+ for (var i = 0; i < path.Length; i++)
+ {
+ current = path[i];
+ if (IsDirectorySeparator(current)
+ && (current != DirectorySeparatorChar
+ // Check for sequential separators past the first position (we need to keep initial two for UNC/extended)
+ || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
+ {
+ normalized = false;
+ break;
+ }
+ }
+
+ if (normalized)
+ {
+ return path;
+ }
+
+ var builder = new StringBuilder(MaxShortPath);
+ var start = 0;
+
+ if (IsDirectorySeparator(path[start]))
+ {
+ start++;
+ builder.Append(DirectorySeparatorChar);
+ }
+
+ for (var i = start; i < path.Length; i++)
+ {
+ current = path[i];
+
+ // If we have a separator
+ if (IsDirectorySeparator(current))
+ {
+ // If the next is a separator, skip adding this
+ if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
+ {
+ continue;
+ }
+
+ // Ensure it is the primary separator
+ current = DirectorySeparatorChar;
+ }
+
+ builder.Append(current);
+ }
+
+ return builder.ToString();
+ }
+ }
+}
diff --git a/System.IO.FileSystem/Properties/AssemblyInfo.cs b/System.IO.FileSystem/Properties/AssemblyInfo.cs
index cc0af07..7b2665b 100644
--- a/System.IO.FileSystem/Properties/AssemblyInfo.cs
+++ b/System.IO.FileSystem/Properties/AssemblyInfo.cs
@@ -17,5 +17,7 @@
////////////////////////////////////////////////////////////////
// update this whenever the native assembly signature changes //
-[assembly: AssemblyNativeVersion("1.0.0.1")]
+[assembly: AssemblyNativeVersion("1.0.0.2")]
////////////////////////////////////////////////////////////////
+
+[assembly: InternalsVisibleTo("NFUnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001001120aa3e809b3da4f65e1b1f65c0a3a1bf6335c39860ca41acb3c48de278c6b63c5df38239ec1f2e32d58cb897c8c174a5f8e78a9c0b6087d3aef373d7d0f3d9be67700fc2a5a38de1fb71b5b6f6046d841ff35abee2e0b0840a6291a312be184eb311baff5fef0ff6895b9a5f2253aed32fb06b819134f6bb9d531488a87ea2")]
\ No newline at end of file
diff --git a/System.IO.FileSystem/System.IO.FileSystem.nfproj b/System.IO.FileSystem/System.IO.FileSystem.nfproj
index 2d3ab49..5192c03 100644
--- a/System.IO.FileSystem/System.IO.FileSystem.nfproj
+++ b/System.IO.FileSystem/System.IO.FileSystem.nfproj
@@ -13,7 +13,7 @@
Library
Properties
512
- System.IO.FileSystem
+ System.IO
System.IO.FileSystem
v1.0
True
@@ -53,6 +53,7 @@
+
@@ -75,6 +76,9 @@
..\packages\nanoFramework.Runtime.Events.1.11.15\lib\nanoFramework.Runtime.Events.dll
True
+
+ ..\packages\nanoFramework.System.Runtime.1.0.6\lib\nanoFramework.System.Runtime.dll
+
..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll
True
diff --git a/System.IO.FileSystem/packages.config b/System.IO.FileSystem/packages.config
index 4ebce24..938a8c7 100644
--- a/System.IO.FileSystem/packages.config
+++ b/System.IO.FileSystem/packages.config
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/System.IO.FileSystem/packages.lock.json b/System.IO.FileSystem/packages.lock.json
index 3f37d48..24c58f7 100644
--- a/System.IO.FileSystem/packages.lock.json
+++ b/System.IO.FileSystem/packages.lock.json
@@ -20,6 +20,12 @@
"resolved": "1.1.52",
"contentHash": "gdExWfWNSl4dgaIoVHHFmhLiRSKAabHA8ueHuErGAWd97qaoN2wSHCtvKqfOu1zuzyccbFpm4HBxVsh6bWMyXw=="
},
+ "nanoFramework.System.Runtime": {
+ "type": "Direct",
+ "requested": "[1.0.6, 1.0.6]",
+ "resolved": "1.0.6",
+ "contentHash": "n87itPUMSsOJkUsdoXr0vhiBTggZBMgCtIIC7c+RsVAhF2u/0TU/h+ZLNyFL8Xhl0taPcTN4LiPPTkI+e95Q/g=="
+ },
"nanoFramework.System.Text": {
"type": "Direct",
"requested": "[1.2.54, 1.2.54]",
diff --git a/nanoFramework.System.IO.FileSystem.nuspec b/nanoFramework.System.IO.FileSystem.nuspec
index 30909a6..93f9823 100644
--- a/nanoFramework.System.IO.FileSystem.nuspec
+++ b/nanoFramework.System.IO.FileSystem.nuspec
@@ -20,8 +20,10 @@ This package requires a target with System.IO.FileSystem v$nativeVersion$ (check
nanoFramework C# csharp netmf netnf System.IO.FileSystem
-
-
+
+
+
+