diff options
Diffstat (limited to 'src/mscorlib/src/System/IO/Path.cs')
-rw-r--r-- | src/mscorlib/src/System/IO/Path.cs | 1435 |
1 files changed, 1435 insertions, 0 deletions
diff --git a/src/mscorlib/src/System/IO/Path.cs b/src/mscorlib/src/System/IO/Path.cs new file mode 100644 index 0000000000..4f7993633b --- /dev/null +++ b/src/mscorlib/src/System/IO/Path.cs @@ -0,0 +1,1435 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*============================================================ +** +** +** +** +** +** Purpose: A collection of path manipulation methods. +** +** +===========================================================*/ + +using System; +using System.Security.Permissions; +using Win32Native = Microsoft.Win32.Win32Native; +using System.Text; +using System.Runtime.InteropServices; +using System.Security; +#if FEATURE_LEGACYSURFACE +using System.Security.Cryptography; +#endif +using System.Runtime.CompilerServices; +using System.Globalization; +using System.Runtime.Versioning; +using System.Diagnostics.Contracts; + +namespace System.IO { + // Provides methods for processing directory strings in an ideally + // cross-platform manner. Most of the methods don't do a complete + // full parsing (such as examining a UNC hostname), but they will + // handle most string operations. + [ComVisible(true)] + public static class Path + { + // Platform specific directory separator character. This is backslash + // ('\') on Windows and slash ('/') on Unix. + // +#if !PLATFORM_UNIX + public static readonly char DirectorySeparatorChar = '\\'; + internal const string DirectorySeparatorCharAsString = "\\"; +#else + public static readonly char DirectorySeparatorChar = '/'; + internal const string DirectorySeparatorCharAsString = "/"; +#endif // !PLATFORM_UNIX + + // Platform specific alternate directory separator character. + // There is only one directory separator char on Unix, + // so the same definition is used for both Unix and Windows. + public static readonly char AltDirectorySeparatorChar = '/'; + + // Platform specific volume separator character. This is colon (':') + // on Windows and MacOS, and slash ('/') on Unix. This is mostly + // useful for parsing paths like "c:\windows" or "MacVolume:System Folder". + // +#if !PLATFORM_UNIX + public static readonly char VolumeSeparatorChar = ':'; +#else + public static readonly char VolumeSeparatorChar = '/'; +#endif // !PLATFORM_UNIX + + // Platform specific invalid list of characters in a path. + // See the "Naming a File" MSDN conceptual docs for more details on + // what is valid in a file name (which is slightly different from what + // is legal in a path name). + // Note: This list is duplicated in CheckInvalidPathChars + [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")] +#if !PLATFORM_UNIX + 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 }; +#else + public static readonly char[] InvalidPathChars = { '\0' }; +#endif // !PLATFORM_UNIX + + // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space. + // String.WhitespaceChars will trim aggressively than what the underlying FS does (for ex, NTFS, FAT). + internal static readonly char[] TrimEndChars = + { + (char)0x09, // Horizontal tab + (char)0x0A, // Line feed + (char)0x0B, // Vertical tab + (char)0x0C, // Form feed + (char)0x0D, // Carriage return + (char)0x20, // Space + (char)0x85, // Next line + (char)0xA0 // Non breaking space + }; + +#if !PLATFORM_UNIX + private static readonly char[] RealInvalidPathChars = PathInternal.InvalidPathChars; + + private static readonly char[] InvalidFileNameChars = { '\"', '<', '>', '|', '\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, ':', '*', '?', '\\', '/' }; +#else + private static readonly char[] RealInvalidPathChars = { '\0' }; + + private static readonly char[] InvalidFileNameChars = { '\0', '/' }; +#endif // !PLATFORM_UNIX + +#if !PLATFORM_UNIX + public static readonly char PathSeparator = ';'; +#else + public static readonly char PathSeparator = ':'; +#endif // !PLATFORM_UNIX + + + // The max total path is 260, and the max individual component length is 255. + // For example, D:\<256 char file name> isn't legal, even though it's under 260 chars. + internal static readonly int MaxPath = PathInternal.MaxShortPath; + + internal static readonly int MaxPathComponentLength = PathInternal.MaxComponentLength; + + // Windows API definitions + internal const int MAX_PATH = 260; // From WinDef.h + internal const int MAX_DIRECTORY_PATH = 248; // cannot create directories greater than 248 characters + + // 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 ".cs"). + // + // 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. If path is null, the function + // returns null. If path does not contain a file extension, + // the new file extension is appended to the path. If extension + // is null, any exsiting extension is removed from path. + // + public static String ChangeExtension(String path, String extension) { + if (path != null) { + CheckInvalidPathChars(path); + + String s = path; + for (int i = path.Length; --i >= 0;) { + char ch = path[i]; + if (ch == '.') { + s = path.Substring(0, i); + break; + } + if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break; + } + if (extension != null && path.Length != 0) { + if (extension.Length == 0 || extension[0] != '.') { + s = s + "."; + } + s = s + extension; + } + return s; + } + return null; + } + + // 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"). + public static String GetDirectoryName(String path) + { + return GetDirectoryNameInternal(path); + } + + [System.Security.SecuritySafeCritical] + private static string GetDirectoryNameInternal(string path) + { + if (path != null) + { + CheckInvalidPathChars(path); + + // Expanding short paths is dangerous in this case as the results will change with the current directory. + // + // Suppose you have a path called "PICTUR~1\Foo". Now suppose you have two folders on disk "C:\Mine\Pictures Of Me" + // and "C:\Yours\Pictures of You". If the current directory is neither you'll get back "PICTUR~1". If it is "C:\Mine" + // get back "Pictures Of Me". "C:\Yours" would give back "Pictures of You". + // + // Because of this and as it isn't documented that short paths are expanded we will not expand short names unless + // we're in legacy mode. + string normalizedPath = NormalizePath(path, fullCheck: false, expandShortPaths: +#if FEATURE_PATHCOMPAT + AppContextSwitches.UseLegacyPathHandling +#else + false +#endif + ); + + // If there are no permissions for PathDiscovery to this path, we should NOT expand the short paths + // as this would leak information about paths to which the user would not have access to. + if (path.Length > 0 +#if FEATURE_CAS_POLICY + // Only do the extra logic if we're not in full trust + && !CodeAccessSecurityEngine.QuickCheckForAllDemands() +#endif + ) + { + try + { + // If we were passed in a path with \\?\ we need to remove it as FileIOPermission does not like it. + string tempPath = RemoveLongPathPrefix(path); + + // FileIOPermission cannot handle paths that contain ? or * + // So we only pass to FileIOPermission the text up to them. + int pos = 0; + while (pos < tempPath.Length && (tempPath[pos] != '?' && tempPath[pos] != '*')) + pos++; + + // GetFullPath will Demand that we have the PathDiscovery FileIOPermission and thus throw + // SecurityException if we don't. + // While we don't use the result of this call we are using it as a consistent way of + // doing the security checks. + if (pos > 0) + GetFullPath(tempPath.Substring(0, pos)); + } + catch (SecurityException) + { + // If the user did not have permissions to the path, make sure that we don't leak expanded short paths + // Only re-normalize if the original path had a ~ in it. + if (path.IndexOf("~", StringComparison.Ordinal) != -1) + { + normalizedPath = NormalizePath(path, fullCheck: false, expandShortPaths: false); + } + } + catch (PathTooLongException) { } + catch (NotSupportedException) { } // Security can throw this on "c:\foo:" + catch (IOException) { } + catch (ArgumentException) { } // The normalizePath with fullCheck will throw this for file: and http: + } + + path = normalizedPath; + + int root = GetRootLength(path); + int i = path.Length; + if (i > root) + { + i = path.Length; + if (i == root) return null; + while (i > root && path[--i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar); + return path.Substring(0, i); + } + } + return null; + } + + // 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) + { + CheckInvalidPathChars(path); + +#if !PLATFORM_UNIX && FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + int i = 0; + int length = path.Length; + + if (length >= 1 && (IsDirectorySeparator(path[0]))) + { + // handles UNC names and directories off current drive's root. + i = 1; + if (length >= 2 && (IsDirectorySeparator(path[1]))) + { + i = 2; + int n = 2; + while (i < length && ((path[i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar) || --n > 0)) i++; + } + } + else if (length >= 2 && path[1] == VolumeSeparatorChar) + { + // handles A:\foo. + i = 2; + if (length >= 3 && (IsDirectorySeparator(path[2]))) i++; + } + return i; + } + else +#endif // !PLATFORM_UNIX && FEATURE_PATHCOMPAT + { + return PathInternal.GetRootLength(path); + } + } + + internal static bool IsDirectorySeparator(char c) { + return (c==DirectorySeparatorChar || c == AltDirectorySeparatorChar); + } + + public static char[] GetInvalidPathChars() + { + return (char[]) RealInvalidPathChars.Clone(); + } + + public static char[] GetInvalidFileNameChars() + { + return (char[]) InvalidFileNameChars.Clone(); + } + + // 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. + // + [Pure] + public static String GetExtension(String path) { + if (path==null) + return null; + + CheckInvalidPathChars(path); + int length = path.Length; + for (int i = length; --i >= 0;) { + char ch = path[i]; + if (ch == '.') + { + if (i != length - 1) + return path.Substring(i, length - i); + else + return String.Empty; + } + if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) + break; + } + return String.Empty; + } + + // Expands the given path to a fully qualified path. The resulting string + // consists of a drive letter, a colon, and a root relative path. This + // function does not verify that the resulting path + // refers to an existing file or directory on the associated volume. + [Pure] + [System.Security.SecuritySafeCritical] + public static String GetFullPath(String path) { + String fullPath = GetFullPathInternal(path); +#if FEATURE_CORECLR + FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.PathDiscovery, path, fullPath); + state.EnsureState(); +#else + FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, fullPath, false, false); +#endif + return fullPath; + } + + [System.Security.SecurityCritical] + internal static String UnsafeGetFullPath(String path) + { + String fullPath = GetFullPathInternal(path); +#if !FEATURE_CORECLR + FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, fullPath, false, false); +#endif + return fullPath; + } + + // This method is package access to let us quickly get a string name + // while avoiding a security check. This also serves a slightly + // different purpose - when we open a file, we need to resolve the + // path into a fully qualified, non-relative path name. This + // method does that, finding the current drive &; directory. But + // as long as we don't return this info to the user, we're good. However, + // the public GetFullPath does need to do a security check. + internal static string GetFullPathInternal(string path) + { + if (path == null) + throw new ArgumentNullException("path"); + Contract.EndContractBlock(); + + string newPath = NormalizePath(path, fullCheck: true); + return newPath; + } + + [System.Security.SecuritySafeCritical] // auto-generated + internal unsafe static string NormalizePath(string path, bool fullCheck) + { + return NormalizePath(path, fullCheck, +#if FEATURE_PATHCOMPAT + AppContextSwitches.BlockLongPaths ? PathInternal.MaxShortPath : +#endif + PathInternal.MaxLongPath); + } + + [System.Security.SecuritySafeCritical] // auto-generated + internal unsafe static string NormalizePath(string path, bool fullCheck, bool expandShortPaths) + { + return NormalizePath(path, fullCheck, +#if FEATURE_PATHCOMPAT + AppContextSwitches.BlockLongPaths ? PathInternal.MaxShortPath : +#endif + PathInternal.MaxLongPath, + expandShortPaths); + } + + [System.Security.SecuritySafeCritical] // auto-generated + internal static string NormalizePath(string path, bool fullCheck, int maxPathLength) + { + return NormalizePath(path, fullCheck, maxPathLength, expandShortPaths: true); + } + + [System.Security.SecuritySafeCritical] + internal static string NormalizePath(string path, bool fullCheck, int maxPathLength, bool expandShortPaths) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + return LegacyNormalizePath(path, fullCheck, maxPathLength, expandShortPaths); + } + else +#endif // FEATURE_APPCOMPAT + { + if (PathInternal.IsExtended(path)) + { + // We can't really know what is valid for all cases of extended paths. + // + // - object names can include other characters as well (':', '/', etc.) + // - even file objects have different rules (pipe names can contain most characters) + // + // As such we will do no further analysis of extended paths to avoid blocking known and unknown + // scenarios as well as minimizing compat breaks should we block now and need to unblock later. + return path; + } + + string normalizedPath = null; + + if (fullCheck == false) + { + // Disabled fullCheck is only called by GetDirectoryName and GetPathRoot. + // Avoid adding addtional callers and try going direct to lighter weight NormalizeDirectorySeparators. + normalizedPath = NewNormalizePathLimitedChecks(path, maxPathLength, expandShortPaths); + } + else + { + normalizedPath = NewNormalizePath(path, maxPathLength, expandShortPaths: true); + } + + if (string.IsNullOrWhiteSpace(normalizedPath)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + return normalizedPath; + } + } + + [System.Security.SecuritySafeCritical] + private static string NewNormalizePathLimitedChecks(string path, int maxPathLength, bool expandShortPaths) + { + string normalized = PathInternal.NormalizeDirectorySeparators(path); + + if (PathInternal.IsPathTooLong(normalized) || PathInternal.AreSegmentsTooLong(normalized)) + throw new PathTooLongException(); + +#if !PLATFORM_UNIX + if (!PathInternal.IsDevice(normalized) && PathInternal.HasInvalidVolumeSeparator(path)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + if (expandShortPaths && normalized.IndexOf('~') != -1) + { + try + { + return LongPathHelper.GetLongPathName(normalized); + } + catch + { + // Don't care if we can't get the long path- might not exist, etc. + } + } +#endif + + return normalized; + } + + /// <summary> + /// Normalize the path and check for bad characters or other invalid syntax. + /// </summary> + [System.Security.SecuritySafeCritical] + [ResourceExposure(ResourceScope.Machine)] + [ResourceConsumption(ResourceScope.Machine)] + private static string NewNormalizePath(string path, int maxPathLength, bool expandShortPaths) + { + Contract.Requires(path != null, "path can't be null"); + + // Embedded null characters are the only invalid character case we want to check up front. + // This is because the nulls will signal the end of the string to Win32 and therefore have + // unpredictable results. Other invalid characters we give a chance to be normalized out. + if (path.IndexOf('\0') != -1) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + +#if !PLATFORM_UNIX + // Note that colon and wildcard checks happen in FileIOPermissions + + // Technically this doesn't matter but we used to throw for this case + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // We don't want to check invalid characters for device format- see comments for extended above + return LongPathHelper.Normalize(path, (uint)maxPathLength, checkInvalidCharacters: !PathInternal.IsDevice(path), expandShortPaths: expandShortPaths); +#else + if (path.Length == 0) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // Expand with current directory if necessary + if (!IsPathRooted(path)) + path = Combine(Directory.GetCurrentDirectory(), path); + + // We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist, + // and turns it into a full path, which we only want if fullCheck is true. + string collapsedString = PathInternal.RemoveRelativeSegments(path); + + if (collapsedString.Length > maxPathLength) + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + + return collapsedString.Length == 0 ? "/" : collapsedString; +#endif // PLATFORM_UNIX + } + +#if FEATURE_PATHCOMPAT + [System.Security.SecurityCritical] // auto-generated + internal unsafe static String LegacyNormalizePath(String path, bool fullCheck, int maxPathLength, bool expandShortPaths) { + + Contract.Requires(path != null, "path can't be null"); + // If we're doing a full path check, trim whitespace and look for + // illegal path characters. + if (fullCheck) { + // Trim whitespace off the end of the string. + // Win32 normalization trims only U+0020. + path = path.TrimEnd(TrimEndChars); + + // Look for illegal path characters. + if (PathInternal.AnyPathHasIllegalCharacters(path)) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + } + + int index = 0; + // We prefer to allocate on the stack for workingset/perf gain. If the + // starting path is less than MaxPath then we can stackalloc; otherwise we'll + // use a StringBuilder (PathHelper does this under the hood). The latter may + // happen in 2 cases: + // 1. Starting path is greater than MaxPath but it normalizes down to MaxPath. + // This is relevant for paths containing escape sequences. In this case, we + // attempt to normalize down to MaxPath, but the caller pays a perf penalty + // since StringBuilder is used. + // 2. IsolatedStorage, which supports paths longer than MaxPath (value given + // by maxPathLength. + PathHelper newBuffer; + if (path.Length + 1 <= MaxPath) { + char* m_arrayPtr = stackalloc char[MaxPath]; + newBuffer = new PathHelper(m_arrayPtr, MaxPath); + } else { + newBuffer = new PathHelper(path.Length + Path.MaxPath, maxPathLength); + } + + uint numSpaces = 0; + uint numDots = 0; + bool fixupDirectorySeparator = false; + // Number of significant chars other than potentially suppressible + // dots and spaces since the last directory or volume separator char + uint numSigChars = 0; + int lastSigChar = -1; // Index of last significant character. + // Whether this segment of the path (not the complete path) started + // with a volume separator char. Reject "c:...". + bool startedWithVolumeSeparator = false; + bool firstSegment = true; + int lastDirectorySeparatorPos = 0; + +#if !PLATFORM_UNIX + bool mightBeShortFileName = false; + + // LEGACY: This code is here for backwards compatibility reasons. It + // ensures that \\foo.cs\bar.cs stays \\foo.cs\bar.cs instead of being + // turned into \foo.cs\bar.cs. + if (path.Length > 0 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) { + newBuffer.Append('\\'); + index++; + lastSigChar = 0; + } +#endif + + // Normalize the string, stripping out redundant dots, spaces, and + // slashes. + while (index < path.Length) { + char currentChar = path[index]; + + // We handle both directory separators and dots specially. For + // directory separators, we consume consecutive appearances. + // For dots, we consume all dots beyond the second in + // succession. All other characters are added as is. In + // addition we consume all spaces after the last other char + // in a directory name up until the directory separator. + + if (currentChar == DirectorySeparatorChar || currentChar == AltDirectorySeparatorChar) { + // If we have a path like "123.../foo", remove the trailing dots. + // However, if we found "c:\temp\..\bar" or "c:\temp\...\bar", don't. + // Also remove trailing spaces from both files & directory names. + // This was agreed on with the OS team to fix undeletable directory + // names ending in spaces. + + // If we saw a '\' as the previous last significant character and + // are simply going to write out dots, suppress them. + // If we only contain dots and slashes though, only allow + // a string like [dot]+ [space]*. Ignore everything else. + // Legal: "\.. \", "\...\", "\. \" + // Illegal: "\.. .\", "\. .\", "\ .\" + if (numSigChars == 0) { + // Dot and space handling + if (numDots > 0) { + // Look for ".[space]*" or "..[space]*" + int start = lastSigChar + 1; + if (path[start] != '.') + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // Only allow "[dot]+[space]*", and normalize the + // legal ones to "." or ".." + if (numDots >= 2) { + // Reject "C:..." + if (startedWithVolumeSeparator && numDots > 2) + + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + if (path[start + 1] == '.') { + // Search for a space in the middle of the + // dots and throw + for(int i=start + 2; i < start + numDots; i++) { + if (path[i] != '.') + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + } + + numDots = 2; + } + else { + if (numDots > 1) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + numDots = 1; + } + } + + if (numDots == 2) { + newBuffer.Append('.'); + } + + newBuffer.Append('.'); + fixupDirectorySeparator = false; + + // Continue in this case, potentially writing out '\'. + } + + if (numSpaces > 0 && firstSegment) { + // Handle strings like " \\server\share". + if (index + 1 < path.Length && + (path[index + 1] == DirectorySeparatorChar || path[index + 1] == AltDirectorySeparatorChar)) + { + newBuffer.Append(DirectorySeparatorChar); + } + } + } + numDots = 0; + numSpaces = 0; // Suppress trailing spaces + + if (!fixupDirectorySeparator) { + fixupDirectorySeparator = true; + newBuffer.Append(DirectorySeparatorChar); + } + numSigChars = 0; + lastSigChar = index; + startedWithVolumeSeparator = false; + firstSegment = false; + +#if !PLATFORM_UNIX + // For short file names, we must try to expand each of them as + // soon as possible. We need to allow people to specify a file + // name that doesn't exist using a path with short file names + // in it, such as this for a temp file we're trying to create: + // C:\DOCUME~1\USERNA~1.RED\LOCALS~1\Temp\bg3ylpzp + // We could try doing this afterwards piece by piece, but it's + // probably a lot simpler to do it here. + if (mightBeShortFileName) { + newBuffer.TryExpandShortFileName(); + mightBeShortFileName = false; + } +#endif + int thisPos = newBuffer.Length - 1; + if (thisPos - lastDirectorySeparatorPos > MaxPathComponentLength) + { + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + } + lastDirectorySeparatorPos = thisPos; + } // if (Found directory separator) + else if (currentChar == '.') { + // Reduce only multiple .'s only after slash to 2 dots. For + // instance a...b is a valid file name. + numDots++; + // Don't flush out non-terminal spaces here, because they may in + // the end not be significant. Turn "c:\ . .\foo" -> "c:\foo" + // which is the conclusion of removing trailing dots & spaces, + // as well as folding multiple '\' characters. + } + else if (currentChar == ' ') { + numSpaces++; + } + else { // Normal character logic +#if !PLATFORM_UNIX + if (currentChar == '~' && expandShortPaths) + mightBeShortFileName = true; +#endif + + fixupDirectorySeparator = false; + +#if !PLATFORM_UNIX + // To reject strings like "C:...\foo" and "C :\foo" + if (firstSegment && currentChar == VolumeSeparatorChar) { + // Only accept "C:", not "c :" or ":" + // Get a drive letter or ' ' if index is 0. + char driveLetter = (index > 0) ? path[index-1] : ' '; + bool validPath = ((numDots == 0) && (numSigChars >= 1) && (driveLetter != ' ')); + if (!validPath) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + startedWithVolumeSeparator = true; + // We need special logic to make " c:" work, we should not fix paths like " foo::$DATA" + if (numSigChars > 1) { // Common case, simply do nothing + int spaceCount = 0; // How many spaces did we write out, numSpaces has already been reset. + while((spaceCount < newBuffer.Length) && newBuffer[spaceCount] == ' ') + spaceCount++; + if (numSigChars - spaceCount == 1) { + //Safe to update stack ptr directly + newBuffer.Length = 0; + newBuffer.Append(driveLetter); // Overwrite spaces, we need a special case to not break " foo" as a relative path. + } + } + numSigChars = 0; + } + else +#endif // !PLATFORM_UNIX + { + numSigChars += 1 + numDots + numSpaces; + } + + // Copy any spaces & dots since the last significant character + // to here. Note we only counted the number of dots & spaces, + // and don't know what order they're in. Hence the copy. + if (numDots > 0 || numSpaces > 0) { + int numCharsToCopy = (lastSigChar >= 0) ? index - lastSigChar - 1 : index; + if (numCharsToCopy > 0) { + for (int i=0; i<numCharsToCopy; i++) { + newBuffer.Append(path[lastSigChar + 1 + i]); + } + } + numDots = 0; + numSpaces = 0; + } + + newBuffer.Append(currentChar); + lastSigChar = index; + } + + index++; + } // end while + + if (newBuffer.Length - 1 - lastDirectorySeparatorPos > MaxPathComponentLength) + { + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + } + + // Drop any trailing dots and spaces from file & directory names, EXCEPT + // we MUST make sure that "C:\foo\.." is correctly handled. + // Also handle "C:\foo\." -> "C:\foo", while "C:\." -> "C:\" + if (numSigChars == 0) { + if (numDots > 0) { + // Look for ".[space]*" or "..[space]*" + int start = lastSigChar + 1; + if (path[start] != '.') + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // Only allow "[dot]+[space]*", and normalize the + // legal ones to "." or ".." + if (numDots >= 2) { + // Reject "C:..." + if (startedWithVolumeSeparator && numDots > 2) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + if (path[start + 1] == '.') { + // Search for a space in the middle of the + // dots and throw + for(int i=start + 2; i < start + numDots; i++) { + if (path[i] != '.') + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + } + + numDots = 2; + } + else { + if (numDots > 1) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + numDots = 1; + } + } + + if (numDots == 2) { + newBuffer.Append('.'); + } + + newBuffer.Append('.'); + } + } // if (numSigChars == 0) + + // If we ended up eating all the characters, bail out. + if (newBuffer.Length == 0) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal")); + + // Disallow URL's here. Some of our other Win32 API calls will reject + // them later, so we might be better off rejecting them here. + // Note we've probably turned them into "file:\D:\foo.tmp" by now. + // But for compatibility, ensure that callers that aren't doing a + // full check aren't rejected here. + if (fullCheck) { + if ( newBuffer.OrdinalStartsWith("http:", false) || + newBuffer.OrdinalStartsWith("file:", false)) + { + throw new ArgumentException(Environment.GetResourceString("Argument_PathUriFormatNotSupported")); + } + } + +#if !PLATFORM_UNIX + // If the last part of the path (file or directory name) had a tilde, + // expand that too. + if (mightBeShortFileName) { + newBuffer.TryExpandShortFileName(); + } +#endif + + // Call the Win32 API to do the final canonicalization step. + int result = 1; + + if (fullCheck) { + // NOTE: Win32 GetFullPathName requires the input buffer to be big enough to fit the initial + // path which is a concat of CWD and the relative path, this can be of an arbitrary + // size and could be > MAX_PATH (which becomes an artificial limit at this point), + // even though the final normalized path after fixing up the relative path syntax + // might be well within the MAX_PATH restriction. For ex, + // "c:\SomeReallyLongDirName(thinkGreaterThan_MAXPATH)\..\foo.txt" which actually requires a + // buffer well with in the MAX_PATH as the normalized path is just "c:\foo.txt" + // This buffer requirement seems wrong, it could be a bug or a perf optimization + // like returning required buffer length quickly or avoid stratch buffer etc. + // Ideally we would get the required buffer length first by calling GetFullPathName + // once without the buffer and use that in the later call but this doesn't always work + // due to Win32 GetFullPathName bug. For instance, in Win2k, when the path we are trying to + // fully qualify is a single letter name (such as "a", "1", ",") GetFullPathName + // fails to return the right buffer size (i.e, resulting in insufficient buffer). + // To workaround this bug we will start with MAX_PATH buffer and grow it once if the + // return value is > MAX_PATH. + + result = newBuffer.GetFullPathName(); + +#if !PLATFORM_UNIX + // If we called GetFullPathName with something like "foo" and our + // command window was in short file name mode (ie, by running edlin or + // DOS versions of grep, etc), we might have gotten back a short file + // name. So, check to see if we need to expand it. + mightBeShortFileName = false; + for(int i=0; i < newBuffer.Length && !mightBeShortFileName; i++) { + if (newBuffer[i] == '~' && expandShortPaths) + mightBeShortFileName = true; + } + + if (mightBeShortFileName) { + bool r = newBuffer.TryExpandShortFileName(); + // Consider how the path "Doesn'tExist" would expand. If + // we add in the current directory, it too will need to be + // fully expanded, which doesn't happen if we use a file + // name that doesn't exist. + if (!r) { + int lastSlash = -1; + + for (int i = newBuffer.Length - 1; i >= 0; i--) { + if (newBuffer[i] == DirectorySeparatorChar) { + lastSlash = i; + break; + } + } + + if (lastSlash >= 0) { + + // This bounds check is for safe memcpy but we should never get this far + if (newBuffer.Length >= maxPathLength) + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + + int lenSavedName = newBuffer.Length - lastSlash - 1; + Contract.Assert(lastSlash < newBuffer.Length, "path unexpectedly ended in a '\'"); + + newBuffer.Fixup(lenSavedName, lastSlash); + } + } + } +#endif // PLATFORM_UNIX + } + + if (result != 0) { + /* Throw an ArgumentException for paths like \\, \\server, \\server\ + This check can only be properly done after normalizing, so + \\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\ + (an internal kernel path) because it provides aliases for drives. */ + if (newBuffer.Length > 1 && newBuffer[0] == '\\' && newBuffer[1] == '\\') { + int startIndex = 2; + while (startIndex < result) { + if (newBuffer[startIndex] == '\\') { + startIndex++; + break; + } + else { + startIndex++; + } + } + if (startIndex == result) + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC")); + + // Check for \\?\Globalroot, an internal mechanism to the kernel + // that provides aliases for drives and other undocumented stuff. + // The kernel team won't even describe the full set of what + // is available here - we don't want managed apps mucking + // with this for security reasons. + if ( newBuffer.OrdinalStartsWith("\\\\?\\globalroot", true)) + throw new ArgumentException(Environment.GetResourceString("Arg_PathGlobalRoot")); + } + } + + // Check our result and form the managed string as necessary. + if (newBuffer.Length >= maxPathLength) + throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong")); + + if (result == 0) { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == 0) + errorCode = Win32Native.ERROR_BAD_PATHNAME; + __Error.WinIOError(errorCode, path); + return null; // Unreachable - silence a compiler error. + } + + return newBuffer.ToStringOrExisting(path); + } +#endif // FEATURE_PATHCOMPAT + + internal const int MaxLongPath = PathInternal.MaxLongPath; + + private const string LongPathPrefix = PathInternal.ExtendedPathPrefix; + private const string UNCPathPrefix = PathInternal.UncPathPrefix; + private const string UNCLongPathPrefixToInsert = PathInternal.UncExtendedPrefixToInsert; + private const string UNCLongPathPrefix = PathInternal.UncExtendedPathPrefix; + + internal static bool HasLongPathPrefix(string path) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + return path.StartsWith(LongPathPrefix, StringComparison.Ordinal); + else +#endif + return PathInternal.IsExtended(path); + } + + internal static string AddLongPathPrefix(string path) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) + return path; + + if (path.StartsWith(UNCPathPrefix, StringComparison.Ordinal)) + return path.Insert(2, UNCLongPathPrefixToInsert); // Given \\server\share in longpath becomes \\?\UNC\server\share => UNCLongPathPrefix + path.SubString(2); => The actual command simply reduces the operation cost. + + return LongPathPrefix + path; + } + else +#endif + { + return PathInternal.EnsureExtendedPrefix(path); + } + } + + internal static string RemoveLongPathPrefix(string path) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal)) + return path; + + if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase)) + return path.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. + + return path.Substring(4); + } + else +#endif + { + return PathInternal.RemoveExtendedPrefix(path); + } + } + + internal static StringBuilder RemoveLongPathPrefix(StringBuilder pathSB) + { +#if FEATURE_PATHCOMPAT + if (AppContextSwitches.UseLegacyPathHandling) + { + if (!PathInternal.StartsWithOrdinal(pathSB, LongPathPrefix)) + return pathSB; + + // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost. + if (PathInternal.StartsWithOrdinal(pathSB, UNCLongPathPrefix, ignoreCase: true)) + return pathSB.Remove(2, 6); + + return pathSB.Remove(0, 4); + } + else +#endif + { + return PathInternal.RemoveExtendedPrefix(pathSB); + } + } + + + // 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. + // + [Pure] + public static String GetFileName(String path) { + if (path != null) { + CheckInvalidPathChars(path); + + int length = path.Length; + for (int i = length; --i >= 0;) { + char ch = path[i]; + if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) + return path.Substring(i + 1, length - i - 1); + + } + } + return path; + } + + [Pure] + public static String GetFileNameWithoutExtension(String path) { + path = GetFileName(path); + if (path != null) + { + int i; + if ((i=path.LastIndexOf('.')) == -1) + return path; // No path extension found + else + return path.Substring(0,i); + } + return null; + } + + + + // Returns the root portion of the given path. The resulting string + // consists of those rightmost characters of the path that constitute the + // root of the path. Possible patterns for the resulting string are: An + // empty string (a relative path on the current drive), "\" (an absolute + // path on the current drive), "X:" (a relative path on a given drive, + // where X is the drive letter), "X:\" (an absolute path on a given drive), + // and "\\server\share" (a UNC path for a given server and share name). + // The resulting string is null if path is null. + // + [Pure] + public static String GetPathRoot(String path) { + if (path == null) return null; + + // Expanding short paths has no impact on the path root- there is no such thing as an + // 8.3 volume or server/share name. + path = NormalizePath(path, fullCheck: false, expandShortPaths: false); + return path.Substring(0, GetRootLength(path)); + } + + [System.Security.SecuritySafeCritical] + public static String GetTempPath() + { +#if !FEATURE_CORECLR + new EnvironmentPermission(PermissionState.Unrestricted).Demand(); +#endif + StringBuilder sb = new StringBuilder(PathInternal.MaxShortPath); + uint r = Win32Native.GetTempPath(PathInternal.MaxShortPath, sb); + String path = sb.ToString(); + if (r==0) __Error.WinIOError(); + path = GetFullPathInternal(path); +#if FEATURE_CORECLR + FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.Write, String.Empty, path); + state.EnsureState(); +#endif + return path; + } + + internal static bool IsRelative(string path) + { + Contract.Assert(path != null, "path can't be null"); + return PathInternal.IsPartiallyQualified(path); + } + + // Returns a cryptographically strong random 8.3 string that can be + // used as either a folder name or a file name. +#if FEATURE_CORECLR + [System.Security.SecuritySafeCritical] +#endif + public static String GetRandomFileName() + { + // 5 bytes == 40 bits == 40/5 == 8 chars in our encoding + // This gives us exactly 8 chars. We want to avoid the 8.3 short name issue + byte[] key = new byte[10]; + +#if FEATURE_CORECLR + Win32Native.Random(true, key, key.Length); +#else + // RNGCryptoServiceProvider is disposable in post-Orcas desktop mscorlibs, but not in CoreCLR's + // mscorlib, so we need to do a manual using block for it. + RNGCryptoServiceProvider rng = null; + try + { + rng = new RNGCryptoServiceProvider(); + + rng.GetBytes(key); + } + finally + { + if (rng != null) + { + rng.Dispose(); + } + } +#endif + + // rndCharArray is expected to be 16 chars + char[] rndCharArray = Path.ToBase32StringSuitableForDirName(key).ToCharArray(); + rndCharArray[8] = '.'; + return new String(rndCharArray, 0, 12); + } + + // Returns a unique temporary file name, and creates a 0-byte file by that + // name on disk. + [System.Security.SecuritySafeCritical] + public static String GetTempFileName() + { + return InternalGetTempFileName(true); + } + + [System.Security.SecurityCritical] + internal static String UnsafeGetTempFileName() + { + return InternalGetTempFileName(false); + } + + [System.Security.SecurityCritical] + private static String InternalGetTempFileName(bool checkHost) + { + String path = GetTempPath(); + + // Since this can write to the temp directory and theoretically + // cause a denial of service attack, demand FileIOPermission to + // that directory. + +#if FEATURE_CORECLR + if (checkHost) + { + FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.Write, String.Empty, path); + state.EnsureState(); + } +#else + FileIOPermission.QuickDemand(FileIOPermissionAccess.Write, path); +#endif + StringBuilder sb = new StringBuilder(MaxPath); + uint r = Win32Native.GetTempFileName(path, "tmp", 0, sb); + if (r==0) __Error.WinIOError(); + return sb.ToString(); + } + + // 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. + // + [Pure] + public static bool HasExtension(String path) { + if (path != null) { + CheckInvalidPathChars(path); + + for (int i = path.Length; --i >= 0;) { + char ch = path[i]; + if (ch == '.') { + if ( i != path.Length - 1) + return true; + else + return false; + } + if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break; + } + } + return false; + } + + // 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 (":"). + // + [Pure] + public static bool IsPathRooted(String path) { + if (path != null) { + CheckInvalidPathChars(path); + + int length = path.Length; +#if !PLATFORM_UNIX + if ((length >= 1 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) || (length >= 2 && path[1] == VolumeSeparatorChar)) + return true; +#else + if (length >= 1 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) + return true; +#endif + } + return false; + } + + public static String Combine(String path1, String path2) { + if (path1==null || path2==null) + throw new ArgumentNullException((path1==null) ? "path1" : "path2"); + Contract.EndContractBlock(); + CheckInvalidPathChars(path1); + CheckInvalidPathChars(path2); + + return CombineNoChecks(path1, path2); + } + + public static String Combine(String path1, String path2, String path3) { + if (path1 == null || path2 == null || path3 == null) + throw new ArgumentNullException((path1 == null) ? "path1" : (path2 == null) ? "path2" : "path3"); + Contract.EndContractBlock(); + CheckInvalidPathChars(path1); + CheckInvalidPathChars(path2); + CheckInvalidPathChars(path3); + + return CombineNoChecks(CombineNoChecks(path1, path2), path3); + } + + public static String Combine(String path1, String path2, String path3, String path4) { + if (path1 == null || path2 == null || path3 == null || path4 == null) + throw new ArgumentNullException((path1 == null) ? "path1" : (path2 == null) ? "path2" : (path3 == null) ? "path3" : "path4"); + Contract.EndContractBlock(); + CheckInvalidPathChars(path1); + CheckInvalidPathChars(path2); + CheckInvalidPathChars(path3); + CheckInvalidPathChars(path4); + + return CombineNoChecks(CombineNoChecks(CombineNoChecks(path1, path2), path3), path4); + } + + public static String Combine(params String[] paths) { + if (paths == null) { + throw new ArgumentNullException("paths"); + } + Contract.EndContractBlock(); + + int finalSize = 0; + int firstComponent = 0; + + // We have two passes, the first calcuates how large a buffer to allocate and does some precondition + // checks on the paths passed in. The second actually does the combination. + + for (int i = 0; i < paths.Length; i++) { + if (paths[i] == null) { + throw new ArgumentNullException("paths"); + } + + if (paths[i].Length == 0) { + continue; + } + + CheckInvalidPathChars(paths[i]); + + if (Path.IsPathRooted(paths[i])) { + firstComponent = i; + finalSize = paths[i].Length; + } else { + finalSize += paths[i].Length; + } + + char ch = paths[i][paths[i].Length - 1]; + if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar) + finalSize++; + } + + StringBuilder finalPath = StringBuilderCache.Acquire(finalSize); + + for (int i = firstComponent; i < paths.Length; i++) { + if (paths[i].Length == 0) { + continue; + } + + if (finalPath.Length == 0) { + finalPath.Append(paths[i]); + } else { + char ch = finalPath[finalPath.Length - 1]; + if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar) { + finalPath.Append(DirectorySeparatorChar); + } + + finalPath.Append(paths[i]); + } + } + + return StringBuilderCache.GetStringAndRelease(finalPath); + } + + private static String CombineNoChecks(String path1, String path2) { + if (path2.Length == 0) + return path1; + + if (path1.Length == 0) + return path2; + + if (IsPathRooted(path2)) + return path2; + + char ch = path1[path1.Length - 1]; + if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar) + return path1 + DirectorySeparatorCharAsString + path2; + return path1 + path2; + } + + private static readonly Char[] s_Base32Char = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '0', '1', '2', '3', '4', '5'}; + + internal static String ToBase32StringSuitableForDirName(byte[] buff) + { + // This routine is optimised to be used with buffs of length 20 + Contract.Assert(((buff.Length % 5) == 0), "Unexpected hash length"); + + StringBuilder sb = StringBuilderCache.Acquire(); + byte b0, b1, b2, b3, b4; + int l, i; + + l = buff.Length; + i = 0; + + // Create l chars using the last 5 bits of each byte. + // Consume 3 MSB bits 5 bytes at a time. + + do + { + b0 = (i < l) ? buff[i++] : (byte)0; + b1 = (i < l) ? buff[i++] : (byte)0; + b2 = (i < l) ? buff[i++] : (byte)0; + b3 = (i < l) ? buff[i++] : (byte)0; + b4 = (i < l) ? buff[i++] : (byte)0; + + // Consume the 5 Least significant bits of each byte + sb.Append(s_Base32Char[b0 & 0x1F]); + sb.Append(s_Base32Char[b1 & 0x1F]); + sb.Append(s_Base32Char[b2 & 0x1F]); + sb.Append(s_Base32Char[b3 & 0x1F]); + sb.Append(s_Base32Char[b4 & 0x1F]); + + // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4 + sb.Append(s_Base32Char[( + ((b0 & 0xE0) >> 5) | + ((b3 & 0x60) >> 2))]); + + sb.Append(s_Base32Char[( + ((b1 & 0xE0) >> 5) | + ((b4 & 0x60) >> 2))]); + + // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4 + + b2 >>= 5; + + Contract.Assert(((b2 & 0xF8) == 0), "Unexpected set bits"); + + if ((b3 & 0x80) != 0) + b2 |= 0x08; + if ((b4 & 0x80) != 0) + b2 |= 0x10; + + sb.Append(s_Base32Char[b2]); + + } while (i < l); + + return StringBuilderCache.GetStringAndRelease(sb); + } + + // ".." can only be used if it is specified as a part of a valid File/Directory name. We disallow + // the user being able to use it to move up directories. Here are some examples eg + // Valid: a..b abc..d + // Invalid: ..ab ab.. .. abc..d\abc.. + // + internal static void CheckSearchPattern(String searchPattern) + { + int index; + while ((index = searchPattern.IndexOf("..", StringComparison.Ordinal)) != -1) { + + if (index + 2 == searchPattern.Length) // Terminal ".." . Files names cannot end in ".." + throw new ArgumentException(Environment.GetResourceString("Arg_InvalidSearchPattern")); + + if ((searchPattern[index+2] == DirectorySeparatorChar) + || (searchPattern[index+2] == AltDirectorySeparatorChar)) + throw new ArgumentException(Environment.GetResourceString("Arg_InvalidSearchPattern")); + + searchPattern = searchPattern.Substring(index + 2); + } + + } + + internal static void CheckInvalidPathChars(String path, bool checkAdditional = false) + { + if (path == null) + throw new ArgumentNullException("path"); + + if (PathInternal.HasIllegalCharacters(path, checkAdditional)) + throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars")); + } + + internal static String InternalCombine(String path1, String path2) { + if (path1==null || path2==null) + throw new ArgumentNullException((path1==null) ? "path1" : "path2"); + Contract.EndContractBlock(); + CheckInvalidPathChars(path1); + CheckInvalidPathChars(path2); + + if (path2.Length == 0) + throw new ArgumentException(Environment.GetResourceString("Argument_PathEmpty"), "path2"); + if (IsPathRooted(path2)) + throw new ArgumentException(Environment.GetResourceString("Arg_Path2IsRooted"), "path2"); + int i = path1.Length; + if (i == 0) return path2; + char ch = path1[i - 1]; + if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar) + return path1 + DirectorySeparatorCharAsString + path2; + return path1 + path2; + } + + } +} |