From 7729e23584b5ca114d8b37f64d0a3e3ad9b622d9 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Wed, 8 Nov 2023 22:20:48 -0800 Subject: [PATCH 01/11] Uncommenting the Assert.SkipTest --- System.IO.FileSystem.UnitTests/FileUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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:\"; From 2d62e7c475178d193a3df3085d82355940ee7337 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Thu, 9 Nov 2023 01:04:47 -0800 Subject: [PATCH 02/11] Removing redundant CheckInvalidPathChars check --- System.IO.FileSystem/File.cs | 3 --- 1 file changed, 3 deletions(-) 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; From dce871705a3f90b9661dfbab3de32ba3865511b0 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Thu, 9 Nov 2023 16:19:27 -0800 Subject: [PATCH 03/11] All methods refactored and validated --- .../nano.runsettings | 2 +- .../packages.config | 8 +- .../packages.lock.json | 24 +- System.IO.FileSystem/Path.cs | 760 ++++++------------ System.IO.FileSystem/PathInternal.cs | 273 +++++++ .../Properties/AssemblyInfo.cs | 3 +- .../System.IO.FileSystem.nfproj | 24 +- System.IO.FileSystem/packages.config | 9 +- System.IO.FileSystem/packages.lock.json | 30 +- 9 files changed, 581 insertions(+), 552 deletions(-) create mode 100644 System.IO.FileSystem/PathInternal.cs 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.UnitTests/packages.config b/System.IO.FileSystem.UnitTests/packages.config index 9f59000..d8897d1 100644 --- a/System.IO.FileSystem.UnitTests/packages.config +++ b/System.IO.FileSystem.UnitTests/packages.config @@ -1,7 +1,7 @@  - - - - + + + + \ No newline at end of file diff --git a/System.IO.FileSystem.UnitTests/packages.lock.json b/System.IO.FileSystem.UnitTests/packages.lock.json index c0e6312..5437776 100644 --- a/System.IO.FileSystem.UnitTests/packages.lock.json +++ b/System.IO.FileSystem.UnitTests/packages.lock.json @@ -4,27 +4,27 @@ ".NETnanoFramework,Version=v1.0": { "nanoFramework.CoreLibrary": { "type": "Direct", - "requested": "[1.14.2, 1.14.2]", - "resolved": "1.14.2", - "contentHash": "j1mrz4mitl5LItvmHMsw1aHzCAfvTTgIkRxA0mhs5mSpctJ/BBcuNwua5j3MspfRNKreCQPy/qZy/D9ADLL/PA==" + "requested": "[1.15.5, 1.15.5]", + "resolved": "1.15.5", + "contentHash": "u2+GvAp1uxLrGdILACAZy+EVKOs28EQ52j8Lz7599egXZ3GBGejjnR2ofhjMQwzrJLlgtyrsx8nSLngDfJNsAg==" }, "nanoFramework.System.IO.Streams": { "type": "Direct", - "requested": "[1.1.38, 1.1.38]", - "resolved": "1.1.38", - "contentHash": "qEtu/lMDtr5kPKc939vO3uX8h+W0/+Qx2N3Zx005JxqGiL71e4ScecEyGPIp8v1MzRd9pkoxInUb6jOAh+eyXA==" + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "gdExWfWNSl4dgaIoVHHFmhLiRSKAabHA8ueHuErGAWd97qaoN2wSHCtvKqfOu1zuzyccbFpm4HBxVsh6bWMyXw==" }, "nanoFramework.System.Text": { "type": "Direct", - "requested": "[1.2.37, 1.2.37]", - "resolved": "1.2.37", - "contentHash": "ORgRq0HSynSBhlXRTHdhzZiOdq/nRhdnX+DeIGw56y9OSc8dvqUz6elm97Jz+4WQ6ikpvs5PFGINAa35kBebwQ==" + "requested": "[1.2.54, 1.2.54]", + "resolved": "1.2.54", + "contentHash": "k3OutSNRMs9di42LQ+5GbpHBY07aMEZWGkaS3Mj3ZU4cWqJc4deFGzRd+LBFQl1mRGdQaM5sl/euTZdcg8R9Zg==" }, "nanoFramework.TestFramework": { "type": "Direct", - "requested": "[2.1.85, 2.1.85]", - "resolved": "2.1.85", - "contentHash": "UAydT9MgZfufPcLpn5KbFZkR3ul6zSt9ERYDPDen7IWxeT+fGpRTes8QTWfhgMsxF3E27b3wI6K6wwWQkeGuFQ==" + "requested": "[2.1.87, 2.1.87]", + "resolved": "2.1.87", + "contentHash": "58nW0UByML7fciTfvLfaEwysf6319SQWmah9v6N3xF9E7jWiHnh3FfEN/wtP/E2wwufmiFZAO2CidbiuEEJmKA==" } } } diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs index ceefc82..1bb5516 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -3,261 +3,219 @@ // See LICENSE file in the project root for full license information. // -using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.FileSystem; 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 = { '?', '*' }; + public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar; - #endregion - - - #region Constructor - - 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) // TODO: Unit test { - 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; + } - if (!Path.IsPathRooted(path)) + while (end > rootLength && !PathInternal.IsDirectorySeparator(path[--end])) { - string currDir = Directory.GetCurrentDirectory(); - path = Path.Combine(currDir, path); } - return NormalizePath(path, false); - */ - throw new NotImplementedException(); + // Trim off any remaining separators (to deal with C:\foo\\bar) + while (end > rootLength && PathInternal.IsDirectorySeparator(path[end - 1])) + { + end--; + } + + 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. - public static string GetExtension(string path) + /// 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) // TODO: Unit test { - if (path == null) + if (path is null) { return null; } - CheckInvalidPathChars(path); - - int length = path.Length; + var 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 +225,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. - public static string GetFileName(string path) + /// 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) // TODO: Unit test { - 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 +261,164 @@ public static string GetFileName(string path) /// Returns the file name of the specified path string without the extension. /// /// The path of the file. - /// - public static string GetFileNameWithoutExtension(string path) + /// The string returned by , minus the last period (.) and all characters following it. + [return: NotNullIfNotNull("path")] + public static string GetFileNameWithoutExtension(string path) // TODO: Unit test { - 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('.'); + + 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 of a file or directory. - /// The root portion of the given path. - /// if path contains invalid characters. - public static string GetPathRoot(string path) + /// An array containing the characters that are not allowed in file names. + public static char[] GetInvalidFileNameChars() => new char[] { - return path == null ? null : path.Substring(0, path.IndexOf(DirectorySeparatorChar)); - } + '\"', '<', '>', '|', '\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 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 path 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 path names. + public static char[] GetInvalidPathChars() => new[] { - 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 the root directory information from the path contained in the specified string. /// - /// The path to test. - /// Boolean indicating whether the path is rooted. - /// if path contains invalid characters. - public static bool IsPathRooted(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) // TODO: Unit test { - if (path != null) + if (PathInternal.IsEffectivelyEmpty(path)) { - CheckInvalidPathChars(path); - - int length = path.Length; - - if (length >= 1 && (path[0] == DirectorySeparatorChar)) - { - return true; - } + return null; } - return false; + var pathRootLength = PathInternal.GetRootLength(path); + var pathRoot = pathRootLength <= 0 ? string.Empty : path.Substring(0, pathRootLength); + + return PathInternal.NormalizeDirectorySeparators(pathRoot); } /// - /// Combines two strings into a path. + /// Determines whether a path includes a file name extension. /// - /// 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) - { - if (-1 != path.IndexOfAny(InvalidPathChars)) - { -#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 - } - } - - - internal static void ValidateNullOrEmpty(string str) + /// 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) // TODO: Unit test { - if (str == null) + if (path is null) { - throw new ArgumentNullException("Path is null."); + return false; } - if (str.Length == 0) - { - throw new ArgumentException("Path length is 0."); - } - } - - internal static string NormalizePath(string path, bool pattern) - { - ValidateNullOrEmpty(path); - - int pathLength = path.Length; - - int i; - - for (i = 0; i < pathLength; i++) + for (var i = path.Length - 1; i >= 0; 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++) + var ch = path[i]; + + if (ch == '.') { - if (path[i] == '\\') - { - break; - } + return i != path.Length - 1; } - 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 (pathLength >= MAX_PATH) + if (PathInternal.IsDirectorySeparator(ch)) { - throw new IOException("", (int)IOException.IOExceptionErrorCode.PathTooLong); + break; } } - string[] pathParts = path.Split(DirectorySeparatorChar); + return false; + } - if (pattern && (pathParts.Length > 1)) + /// + /// 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) { - throw new ArgumentException("Path contains only a Directory/FileName"); + return false; } - ArrayList finalPathSegments = new ArrayList(); - int pathPartLen; + 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 < pathParts.Length; e++) - { - pathPartLen = pathParts[e].Length; + private static string JoinInternal(string first, string second) + { + Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths"); - 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); - } + var hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1]) + || PathInternal.IsDirectorySeparator(second[0]); - if (pathParts[e].IndexOfAny(InvalidPathChars) != -1) - { - throw new ArgumentException("Path contains invalid characters: " + pathParts[e]); - } - - if (!pattern - && pathParts[e].IndexOfAny(m_illegalCharacters) != -1) - { - throw new ArgumentException("Path contains illegal characters: " + pathParts[e]); - } + return hasSeparator ? + string.Concat(first, second) : + string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second); + } - // 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 (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 = ""; - if (rootedPath) - { - normalizedPath += @"\"; - } - else if (serverPath) - { - normalizedPath += @"\\"; - // btw, server path must specify server name. - if (finalPathSegments.Count == 0) - { - throw new ArgumentException("Server Path is missing server name."); - } - } - bool firstSegment = true; - for (int e = 0; e < finalPathSegments.Count; e++) - { - if (!firstSegment) - { - normalizedPath += "\\"; - } - else - { - firstSegment = false; - } - normalizedPath += (string)finalPathSegments[e]; - } - return normalizedPath; - } + // TODO: Remove these after review - #endregion + // From FS_decl.h + // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method + //private const int FSMaxPathLength = 260 - 2; + // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method + //private const int FSMaxFilenameLength = 256; + // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method + //private const int FSNameMaxLength = 7 + 1; + // Windows API definitions + // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method + //internal const int MAX_PATH = 260; // From WinDef.h } } diff --git a/System.IO.FileSystem/PathInternal.cs b/System.IO.FileSystem/PathInternal.cs new file mode 100644 index 0000000..58a4240 --- /dev/null +++ b/System.IO.FileSystem/PathInternal.cs @@ -0,0 +1,273 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace System.IO.FileSystem +{ + /// 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; + // \\?\, \\.\, \??\ + internal const int DevicePrefixLength = 4; + // \\ + internal const int UncPrefixLength = 2; + // \\?\UNC\, \\.\UNC\ + internal const int UncExtendedPrefixLength = 8; + + /// + /// 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; + + 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++; + } + } + + return i; + } + + /// + /// 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'; + } + + /// + /// 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; + } + + /// + /// 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] == '\\'; + } + + /// + /// 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..83444ae 100644 --- a/System.IO.FileSystem/Properties/AssemblyInfo.cs +++ b/System.IO.FileSystem/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -17,5 +16,5 @@ //////////////////////////////////////////////////////////////// // update this whenever the native assembly signature changes // -[assembly: AssemblyNativeVersion("1.0.0.1")] +[assembly: AssemblyNativeVersion("1.0.0.2")] //////////////////////////////////////////////////////////////// diff --git a/System.IO.FileSystem/System.IO.FileSystem.nfproj b/System.IO.FileSystem/System.IO.FileSystem.nfproj index afbf4e2..332404b 100644 --- a/System.IO.FileSystem/System.IO.FileSystem.nfproj +++ b/System.IO.FileSystem/System.IO.FileSystem.nfproj @@ -53,6 +53,7 @@ + @@ -67,21 +68,20 @@ - - ..\packages\nanoFramework.CoreLibrary.1.14.2\lib\mscorlib.dll - True + + ..\packages\nanoFramework.CoreLibrary.1.15.5\lib\mscorlib.dll - - ..\packages\nanoFramework.Runtime.Events.1.11.6\lib\nanoFramework.Runtime.Events.dll - True + + ..\packages\nanoFramework.Runtime.Events.1.11.15\lib\nanoFramework.Runtime.Events.dll - - ..\packages\nanoFramework.System.Text.1.2.37\lib\nanoFramework.System.Text.dll - True + + ..\packages\nanoFramework.System.Runtime.1.0.6\lib\nanoFramework.System.Runtime.dll - - ..\packages\nanoFramework.System.IO.Streams.1.1.38\lib\System.IO.Streams.dll - True + + ..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll + + + ..\packages\nanoFramework.System.IO.Streams.1.1.52\lib\System.IO.Streams.dll diff --git a/System.IO.FileSystem/packages.config b/System.IO.FileSystem/packages.config index 1adc34d..938a8c7 100644 --- a/System.IO.FileSystem/packages.config +++ b/System.IO.FileSystem/packages.config @@ -1,8 +1,9 @@  - - - - + + + + + \ No newline at end of file diff --git a/System.IO.FileSystem/packages.lock.json b/System.IO.FileSystem/packages.lock.json index f2daf4b..24c58f7 100644 --- a/System.IO.FileSystem/packages.lock.json +++ b/System.IO.FileSystem/packages.lock.json @@ -4,27 +4,33 @@ ".NETnanoFramework,Version=v1.0": { "nanoFramework.CoreLibrary": { "type": "Direct", - "requested": "[1.14.2, 1.14.2]", - "resolved": "1.14.2", - "contentHash": "j1mrz4mitl5LItvmHMsw1aHzCAfvTTgIkRxA0mhs5mSpctJ/BBcuNwua5j3MspfRNKreCQPy/qZy/D9ADLL/PA==" + "requested": "[1.15.5, 1.15.5]", + "resolved": "1.15.5", + "contentHash": "u2+GvAp1uxLrGdILACAZy+EVKOs28EQ52j8Lz7599egXZ3GBGejjnR2ofhjMQwzrJLlgtyrsx8nSLngDfJNsAg==" }, "nanoFramework.Runtime.Events": { "type": "Direct", - "requested": "[1.11.6, 1.11.6]", - "resolved": "1.11.6", - "contentHash": "xkltRh/2xKaZ9zmPHbVr32s1k+e17AInUBhzxKKkUDicJKF39yzTShSklb1OL6DBER5z71SpkGLyl9IdMK9l6w==" + "requested": "[1.11.15, 1.11.15]", + "resolved": "1.11.15", + "contentHash": "3uDNSTfiaewDAyi6fOMWYru0JCn/gr8DEv+Ro/V12SzojU9Dyxl5nSVOBtBXts7vErfIthB6SPiK180AMnrI8A==" }, "nanoFramework.System.IO.Streams": { "type": "Direct", - "requested": "[1.1.38, 1.1.38]", - "resolved": "1.1.38", - "contentHash": "qEtu/lMDtr5kPKc939vO3uX8h+W0/+Qx2N3Zx005JxqGiL71e4ScecEyGPIp8v1MzRd9pkoxInUb6jOAh+eyXA==" + "requested": "[1.1.52, 1.1.52]", + "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.37, 1.2.37]", - "resolved": "1.2.37", - "contentHash": "ORgRq0HSynSBhlXRTHdhzZiOdq/nRhdnX+DeIGw56y9OSc8dvqUz6elm97Jz+4WQ6ikpvs5PFGINAa35kBebwQ==" + "requested": "[1.2.54, 1.2.54]", + "resolved": "1.2.54", + "contentHash": "k3OutSNRMs9di42LQ+5GbpHBY07aMEZWGkaS3Mj3ZU4cWqJc4deFGzRd+LBFQl1mRGdQaM5sl/euTZdcg8R9Zg==" }, "Nerdbank.GitVersioning": { "type": "Direct", From e989ea6ae51b92e609908176d3f34ffee3357180 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Thu, 9 Nov 2023 21:48:37 -0800 Subject: [PATCH 04/11] Adding more unit tests --- .../PathUnitTests.cs | 210 ++++++++++++++++++ .../System.IO.FileSystem.UnitTests.nfproj | 26 +-- System.IO.FileSystem/Path.cs | 15 +- 3 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 System.IO.FileSystem.UnitTests/PathUnitTests.cs diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs new file mode 100644 index 0000000..06b85f7 --- /dev/null +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -0,0 +1,210 @@ +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(actual, path1); + } + + [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(actual, path2); + } + + [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(actual, path2); + } + + [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 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(@"\\server\share\directory\file")); + Assert.AreEqual("file.ext", Path.GetFileName(@"\\server\share\directory\file.ext")); + 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_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(@"\\server\share\directory\file")); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file.ext")); + 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_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() + { + Assert.AreEqual(@"\\server\share", Path.GetPathRoot(@"\\server\share\directory\file")); + Assert.AreEqual(@"\\server\share", Path.GetPathRoot(@"\\server\share\directory\file.ext")); + Assert.AreEqual("I:", Path.GetPathRoot("I:")); + Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\directory\file")); + Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\directory\file.ext")); + Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\file")); + Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\file.ext")); + } + + [TestMethod] + public void HasExtension_returns_false() + { + Assert.IsFalse(Path.HasExtension("file"), "file"); + Assert.IsFalse(Path.HasExtension("file."), "file."); + Assert.IsFalse(Path.HasExtension(@"\"), @"\"); + Assert.IsFalse(Path.HasExtension("/"), "/"); + Assert.IsFalse(Path.HasExtension("I:"), "I:"); + Assert.IsFalse(Path.HasExtension(@"I:\"), @"I:\"); + } + + [TestMethod] + public void HasExtension_returns_true() + { + Assert.IsTrue(Path.HasExtension("file.ext"), "file.ext"); + Assert.IsTrue(Path.HasExtension(@"\file.ext"), @"\file.ext"); + Assert.IsTrue(Path.HasExtension("/file.ext"), "/file.ext"); + Assert.IsTrue(Path.HasExtension("I:file.ext"), "I:file.ext"); + Assert.IsTrue(Path.HasExtension(@"I:\file.ext"), @"I:\file.ext"); + } + + [TestMethod] + public void IsPathRooted_returns_true() + { + Assert.IsTrue(Path.IsPathRooted(@"\")); + Assert.IsTrue(Path.IsPathRooted("/")); + Assert.IsTrue(Path.IsPathRooted("I:")); + Assert.IsTrue(Path.IsPathRooted(@"I:\")); + Assert.IsTrue(Path.IsPathRooted(@"I:\file.ext")); + } + } +} diff --git a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj index fb8a0c8..410563c 100644 --- a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj +++ b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj @@ -29,6 +29,7 @@ + @@ -42,25 +43,20 @@ - - ..\packages\nanoFramework.CoreLibrary.1.14.2\lib\mscorlib.dll - True + + ..\packages\nanoFramework.CoreLibrary.1.15.5\lib\mscorlib.dll - - ..\packages\nanoFramework.System.Text.1.2.37\lib\nanoFramework.System.Text.dll - True + + ..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll - - ..\packages\nanoFramework.TestFramework.2.1.85\lib\nanoFramework.TestFramework.dll - True + + ..\packages\nanoFramework.TestFramework.2.1.87\lib\nanoFramework.TestFramework.dll - - ..\packages\nanoFramework.TestFramework.2.1.85\lib\nanoFramework.UnitTestLauncher.exe - True + + ..\packages\nanoFramework.TestFramework.2.1.87\lib\nanoFramework.UnitTestLauncher.exe - - ..\packages\nanoFramework.System.IO.Streams.1.1.38\lib\System.IO.Streams.dll - True + + ..\packages\nanoFramework.System.IO.Streams.1.1.52\lib\System.IO.Streams.dll diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs index 1bb5516..6538e7b 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -234,7 +234,7 @@ public static string GetExtension(string path) // TODO: Unit test /// If is , this method returns . /// [return: NotNullIfNotNull("path")] - public static string GetFileName(string path) // TODO: Unit test + public static string GetFileName(string path) { if (path is null) { @@ -263,7 +263,7 @@ public static string GetFileName(string path) // TODO: Unit test /// 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) // TODO: Unit test + public static string GetFileNameWithoutExtension(string path) { if (path is null) { @@ -271,8 +271,11 @@ public static string GetFileNameWithoutExtension(string path) // TODO: Unit test } var fileName = GetFileName(path); - var lastPeriod = fileName.LastIndexOf('.'); - + Console.WriteLine($"Filename: '{fileName}'"); + // TODO: Fix this in string as it should just automatically return -1 for a zero length string + var lastPeriod = fileName.Length > 0 ? fileName.LastIndexOf('.') : -1; + Console.WriteLine($"Last period: {lastPeriod}"); + return lastPeriod < 0 ? fileName : // No extension was found fileName.Substring(0, lastPeriod); @@ -319,7 +322,7 @@ public static char[] GetInvalidPathChars() => new[] /// /// if is or is effectively empty. /// - public static string GetPathRoot(string path) // TODO: Unit test + public static string GetPathRoot(string path) { if (PathInternal.IsEffectivelyEmpty(path)) { @@ -340,7 +343,7 @@ public static string GetPathRoot(string path) // TODO: Unit test /// 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) // TODO: Unit test + public static bool HasExtension([NotNullWhen(true)] string path) { if (path is null) { From 3d34fb53d876f61c84d1a53b4c6300c13055ea45 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Fri, 10 Nov 2023 12:56:02 -0800 Subject: [PATCH 05/11] Completing unit test coverage --- .../PathUnitTests.cs | 131 +++++++++++++++--- System.IO.FileSystem/Path.cs | 9 +- 2 files changed, 111 insertions(+), 29 deletions(-) diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs index 06b85f7..ad00120 100644 --- a/System.IO.FileSystem.UnitTests/PathUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -100,6 +100,66 @@ 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", @"\\server\share\", @"\\server\share\file.ext" }; + var answers = new[] { @"I:\", @"I:\directory", @"I:\directory", @"\\server\share", @"\\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:\", @"\\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}")); + 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() { @@ -167,44 +227,69 @@ public void GetPathRoot_returns_null() [TestMethod] public void GetPathRoot_returns_root() { - Assert.AreEqual(@"\\server\share", Path.GetPathRoot(@"\\server\share\directory\file")); - Assert.AreEqual(@"\\server\share", Path.GetPathRoot(@"\\server\share\directory\file.ext")); - Assert.AreEqual("I:", Path.GetPathRoot("I:")); - Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\directory\file")); - Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\directory\file.ext")); - Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\file")); - Assert.AreEqual(@"I:\", Path.GetPathRoot(@"I:\file.ext")); + var tests = new[] + { + @"\\server\share\directory\file", @"\\server\share\directory\file.ext", "I:", @"I:\directory\file", + @"I:\directory\file.ext", @"I:\file", @"I:\file.ext" + }; + + var answers = new[] { @"\\server\share", @"\\server\share", "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 HasExtension_returns_false() { - Assert.IsFalse(Path.HasExtension("file"), "file"); - Assert.IsFalse(Path.HasExtension("file."), "file."); - Assert.IsFalse(Path.HasExtension(@"\"), @"\"); - Assert.IsFalse(Path.HasExtension("/"), "/"); - Assert.IsFalse(Path.HasExtension("I:"), "I:"); - Assert.IsFalse(Path.HasExtension(@"I:\"), @"I:\"); + var tests = new[] + { + "file", @"\file.", @"\", "/", "I:", @"I:\", @"I:\directory\", @"\\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() { - Assert.IsTrue(Path.HasExtension("file.ext"), "file.ext"); - Assert.IsTrue(Path.HasExtension(@"\file.ext"), @"\file.ext"); - Assert.IsTrue(Path.HasExtension("/file.ext"), "/file.ext"); - Assert.IsTrue(Path.HasExtension("I:file.ext"), "I:file.ext"); - Assert.IsTrue(Path.HasExtension(@"I:\file.ext"), @"I:\file.ext"); + var tests = new[] + { + "file.ext", @"\file.ext", "/file.ext", "I:file.ext", @"I:\file.ext", @"I:\directory\file.ext", + @"\\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() { - Assert.IsTrue(Path.IsPathRooted(@"\")); - Assert.IsTrue(Path.IsPathRooted("/")); - Assert.IsTrue(Path.IsPathRooted("I:")); - Assert.IsTrue(Path.IsPathRooted(@"I:\")); - Assert.IsTrue(Path.IsPathRooted(@"I:\file.ext")); + var tests = new[] + { + @"\", "/", "I:", @"I:\", @"I:\file.ext", @"I:\directory\file.ext", @"\\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/Path.cs b/System.IO.FileSystem/Path.cs index 6538e7b..befadf2 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -149,7 +149,7 @@ private static string CombineInternal(string first, string second) /// /// Directory separators are normalized in the returned string. /// - public static string GetDirectoryName(string path) // TODO: Unit test + public static string GetDirectoryName(string path) { if (path is null || PathInternal.IsEffectivelyEmpty(path)) { @@ -192,7 +192,7 @@ internal static int GetDirectoryNameOffset(string path) /// If is , returns . /// If path does not have extension information, returns . [return: NotNullIfNotNull("path")] - public static string GetExtension(string path) // TODO: Unit test + public static string GetExtension(string path) { if (path is null) { @@ -271,10 +271,7 @@ public static string GetFileNameWithoutExtension(string path) } var fileName = GetFileName(path); - Console.WriteLine($"Filename: '{fileName}'"); - // TODO: Fix this in string as it should just automatically return -1 for a zero length string - var lastPeriod = fileName.Length > 0 ? fileName.LastIndexOf('.') : -1; - Console.WriteLine($"Last period: {lastPeriod}"); + var lastPeriod = fileName.LastIndexOf('.'); return lastPeriod < 0 ? fileName : // No extension was found From 6039dbccb29cb4304ac362896a44d78522b5c6bd Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Fri, 10 Nov 2023 13:03:52 -0800 Subject: [PATCH 06/11] Fixing nuspec --- nanoFramework.System.IO.FileSystem.nuspec | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 - - + + + + From 923002cfaff790691f8cb4bc8c5cf8648f827ec4 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Mon, 13 Nov 2023 18:37:07 -0800 Subject: [PATCH 07/11] Fixing order of Assert.AreEqual parameters --- System.IO.FileSystem.UnitTests/PathUnitTests.cs | 6 +++--- System.IO.FileSystem/System.IO.FileSystem.nfproj | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs index ad00120..434df0b 100644 --- a/System.IO.FileSystem.UnitTests/PathUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -54,7 +54,7 @@ public void Combine_returns_path1_if_path2_is_empty_string() var actual = Path.Combine(path1, path2); - Assert.AreEqual(actual, path1); + Assert.AreEqual(path1, actual); } [TestMethod] @@ -74,7 +74,7 @@ public void Combine_returns_path2_if_it_is_an_absolute_path() var actual = Path.Combine(path1, path2); - Assert.AreEqual(actual, path2); + Assert.AreEqual(path2, actual); } [TestMethod] @@ -85,7 +85,7 @@ public void Combine_returns_path2_if_path1_is_empty_string() var actual = Path.Combine(path1, path2); - Assert.AreEqual(actual, path2); + Assert.AreEqual(path2, actual); } [TestMethod] diff --git a/System.IO.FileSystem/System.IO.FileSystem.nfproj b/System.IO.FileSystem/System.IO.FileSystem.nfproj index 9fe7ec6..4a48167 100644 --- a/System.IO.FileSystem/System.IO.FileSystem.nfproj +++ b/System.IO.FileSystem/System.IO.FileSystem.nfproj @@ -76,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 From ac3939f011852f23d39593d1accf48227201c4f0 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Tue, 14 Nov 2023 08:55:33 -0800 Subject: [PATCH 08/11] Fixing namespace --- System.IO.FileSystem/Path.cs | 1 - System.IO.FileSystem/PathInternal.cs | 2 +- System.IO.FileSystem/System.IO.FileSystem.nfproj | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs index befadf2..b053a1e 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO.FileSystem; namespace System.IO { diff --git a/System.IO.FileSystem/PathInternal.cs b/System.IO.FileSystem/PathInternal.cs index 58a4240..88ec1ee 100644 --- a/System.IO.FileSystem/PathInternal.cs +++ b/System.IO.FileSystem/PathInternal.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text; -namespace System.IO.FileSystem +namespace System.IO { /// Contains internal path helpers that are shared between many projects. internal static class PathInternal diff --git a/System.IO.FileSystem/System.IO.FileSystem.nfproj b/System.IO.FileSystem/System.IO.FileSystem.nfproj index 4a48167..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 From 1cb71b05d4789709ecf85998f273606d35e24c84 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Tue, 14 Nov 2023 09:01:52 -0800 Subject: [PATCH 09/11] Adding unit test for `PathInternal.IsValidDriveChar` --- .../PathInternalUnitTests.cs | 19 ++++++++++++++++++ .../System.IO.FileSystem.UnitTests.nfproj | 10 +++++++++ System.IO.FileSystem.UnitTests/key.snk | Bin 0 -> 596 bytes .../Properties/AssemblyInfo.cs | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs create mode 100644 System.IO.FileSystem.UnitTests/key.snk 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/System.IO.FileSystem.UnitTests.nfproj b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj index b2e5b24..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,22 @@ 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 0000000000000000000000000000000000000000..67c9bb0ad77fd9cfb31a5fe1f8e4f6537f8883f8 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096IAgVrqn?0oVUK<}}z@wqRV>QE=V9G(P zv&4>n;N)r`28$iZ}__~(k83t)%SuJd!((DT{8XK)X~rK64E`*F3l z_5^K&AM;wi;^44^3SyC?622Htvk|)gU+)k0iCdaqB2%u@FR*KYku>(Zol_KusD7d= zA40qu=-~L94PXsGJ#eO@FlCr&O~0PO!6o-*GQq`zy7)g4B)IFgLhI0vfH=+L6i;`a zn(iWy{`8{{oj}vGf9S9M=48~NC!$coEAg@m^H3NXd_JrnT}HOd0AR%m^g zLg4+2jC7g-iYgo9N=!v@nv=I99O}eXu|AyULz5*Z*LDuQt4pP~UX|Zf`1Y#v4}kU8;^ac8l5!?oeMbz%wA_dHw*Ud81*FK}w1r zw3t~`4#bY+anQY5XU!i9#lc>IiLAxX~!c5@{0(|Buf%3XVh2FE%G?X~1 z7e5uWO?|rx0b=vcr3gpq3{-d$I8fk&Qrx<8j#tO3HOsxjGMJn4IT0*|11Oqu{gBd3 z&+6?KA|)R~alAn@8a!Y9Eq$TwFM@mu$U=dJ?PzCQY8jWLBlv^YEFIb8Fvhr*vMTOc zG+a5pRFEs9BT_>6MCF{WdN&DTsp^F!N=e1<>cRNkafH_Fr3`v9f%5G2(}LQ$j`vCM iwz?1qR;-^j3D_^H<{zl%^r6ivhR`zlIvhi1+jkY8izavg literal 0 HcmV?d00001 diff --git a/System.IO.FileSystem/Properties/AssemblyInfo.cs b/System.IO.FileSystem/Properties/AssemblyInfo.cs index 83444ae..7b2665b 100644 --- a/System.IO.FileSystem/Properties/AssemblyInfo.cs +++ b/System.IO.FileSystem/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -18,3 +19,5 @@ // update this whenever the native assembly signature changes // [assembly: AssemblyNativeVersion("1.0.0.2")] //////////////////////////////////////////////////////////////// + +[assembly: InternalsVisibleTo("NFUnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001001120aa3e809b3da4f65e1b1f65c0a3a1bf6335c39860ca41acb3c48de278c6b63c5df38239ec1f2e32d58cb897c8c174a5f8e78a9c0b6087d3aef373d7d0f3d9be67700fc2a5a38de1fb71b5b6f6046d841ff35abee2e0b0840a6291a312be184eb311baff5fef0ff6895b9a5f2253aed32fb06b819134f6bb9d531488a87ea2")] \ No newline at end of file From f040a01aee825e403046696db8446fd3fe70a15a Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Wed, 15 Nov 2023 08:44:08 -0800 Subject: [PATCH 10/11] Splitting out UNC test cases --- .../PathUnitTests.cs | 138 ++++++++++++++++-- 1 file changed, 122 insertions(+), 16 deletions(-) diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs index 434df0b..8630a8a 100644 --- a/System.IO.FileSystem.UnitTests/PathUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -103,8 +103,23 @@ public void Combine_throws_if_path2_is_null() [TestMethod] public void GetDirectoryName_returns_directory() { - var tests = new[] { @"I:\directory", @"I:\directory\", @"I:\directory\file.ext", @"\\server\share\", @"\\server\share\file.ext" }; - var answers = new[] { @"I:\", @"I:\directory", @"I:\directory", @"\\server\share", @"\\server\share", @"\\server\share" }; + 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++) { @@ -121,7 +136,21 @@ 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:\", @"\\server\share" }; + 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() + { + + var tests = new[] { @"\\server\share" }; foreach (var test in tests) { var actual = Path.GetDirectoryName(test); @@ -130,7 +159,6 @@ public void GetDirectoryName_returns_null() Assert.IsNull(Path.GetDirectoryName(test), message); } } - [TestMethod] public void GetExtension_returns_empty_string() { @@ -150,6 +178,14 @@ public void GetExtension_returns_extension() 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}")); } @@ -170,14 +206,19 @@ public void GetFilename_returns_empty_string() [TestMethod] public void GetFilename_returns_filename_without_extension() { - Assert.AreEqual("file", Path.GetFileName(@"\\server\share\directory\file")); - Assert.AreEqual("file.ext", Path.GetFileName(@"\\server\share\directory\file.ext")); 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() { @@ -194,14 +235,19 @@ public void GetFilenameWithoutExtension_returns_empty_string() [TestMethod] public void GetFilenameWithoutExtension_returns_filename_without_extension() { - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file")); - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file.ext")); 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() { @@ -229,11 +275,29 @@ public void GetPathRoot_returns_root() { var tests = new[] { - @"\\server\share\directory\file", @"\\server\share\directory\file.ext", "I:", @"I:\directory\file", - @"I:\directory\file.ext", @"I:\file", @"I:\file.ext" + "I:", @"I:\directory\file", @"I:\directory\file.ext", @"I:\file", @"I:\file.ext" }; - var answers = new[] { @"\\server\share", @"\\server\share", "I:", @"I:\", @"I:\", @"I:\", @"I:\" }; + 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() + { + 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++) { @@ -249,8 +313,22 @@ public void HasExtension_returns_false() { var tests = new[] { - "file", @"\file.", @"\", "/", "I:", @"I:\", @"I:\directory\", @"\\server\share\file.", - @"\\server\share\directory\file" + "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++) @@ -265,7 +343,21 @@ public void HasExtension_returns_true() { var tests = new[] { - "file.ext", @"\file.ext", "/file.ext", "I:file.ext", @"I:\file.ext", @"I:\directory\file.ext", + "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" }; @@ -281,8 +373,22 @@ public void IsPathRooted_returns_true() { var tests = new[] { - @"\", "/", "I:", @"I:\", @"I:\file.ext", @"I:\directory\file.ext", @"\\server\share", - @"\\server\share\file.ext", @"\\server\share\directory\file.ext" + @"\", "/", "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++) From 1063445f3fabd777115ba76bafc7c1fa34bab7a1 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Wed, 15 Nov 2023 09:06:48 -0800 Subject: [PATCH 11/11] Removing support for UNC paths --- .../PathUnitTests.cs | 3 +++ System.IO.FileSystem/Path.cs | 27 ------------------- System.IO.FileSystem/PathInternal.cs | 26 ++++++++++++++++++ 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs index 8630a8a..903e32d 100644 --- a/System.IO.FileSystem.UnitTests/PathUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -149,6 +149,7 @@ public void GetDirectoryName_returns_null() [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) @@ -292,6 +293,8 @@ public void GetPathRoot_returns_root() [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" diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs index b053a1e..d04c4ba 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -392,32 +392,5 @@ private static string JoinInternal(string first, string second) string.Concat(first, second) : string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second); } - - - - - - - - - - - - - - - // TODO: Remove these after review - - // From FS_decl.h - // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method - //private const int FSMaxPathLength = 260 - 2; - // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method - //private const int FSMaxFilenameLength = 256; - // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method - //private const int FSNameMaxLength = 7 + 1; - - // Windows API definitions - // Was used in NormalizePath (now called NormalizeDirectorySeparators). The new method does not constrain the length because that is not a concern of the method - //internal const int MAX_PATH = 260; // From WinDef.h } } diff --git a/System.IO.FileSystem/PathInternal.cs b/System.IO.FileSystem/PathInternal.cs index 88ec1ee..92df7ed 100644 --- a/System.IO.FileSystem/PathInternal.cs +++ b/System.IO.FileSystem/PathInternal.cs @@ -15,12 +15,15 @@ internal static class PathInternal 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.). @@ -30,6 +33,7 @@ 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); @@ -84,10 +88,29 @@ internal static int GetRootLength(string path) 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 "\??\") /// @@ -118,6 +141,7 @@ internal static bool IsDeviceUNC(string path) && path[5] == 'N' && path[6] == 'C'; } +#endif /// /// True if the given character is a directory separator. @@ -151,6 +175,7 @@ internal static bool IsEffectivelyEmpty(string path) 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 @@ -166,6 +191,7 @@ internal static bool IsExtended(string path) && path[2] == '?' && path[3] == '\\'; } +#endif /// /// Returns true if the given character is a valid drive letter