Collapse AsSpan().Slice(...) into .AsSpan(...) (dotnet/corefx#27867)
[platform/upstream/coreclr.git] / src / mscorlib / shared / System / IO / Path.Windows.cs
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4
5 using System.Diagnostics;
6 using System.Text;
7
8 namespace System.IO
9 {
10     public static partial class Path
11     {
12         public static char[] GetInvalidFileNameChars() => new char[]
13         {
14             '\"', '<', '>', '|', '\0',
15             (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
16             (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
17             (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
18             (char)31, ':', '*', '?', '\\', '/'
19         };
20
21         public static char[] GetInvalidPathChars() => new char[]
22         {
23             '|', '\0',
24             (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
25             (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
26             (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
27             (char)31
28         };
29
30         // Expands the given path to a fully qualified path.
31         public static string GetFullPath(string path)
32         {
33             if (path == null)
34                 throw new ArgumentNullException(nameof(path));
35
36             // If the path would normalize to string empty, we'll consider it empty
37             if (PathInternal.IsEffectivelyEmpty(path))
38                 throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
39
40             // Embedded null characters are the only invalid character case we trully care about.
41             // This is because the nulls will signal the end of the string to Win32 and therefore have
42             // unpredictable results.
43             if (path.IndexOf('\0') != -1)
44                 throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
45
46             if (PathInternal.IsExtended(path))
47             {
48                 // \\?\ paths are considered normalized by definition. Windows doesn't normalize \\?\
49                 // paths and neither should we. Even if we wanted to GetFullPathName does not work
50                 // properly with device paths. If one wants to pass a \\?\ path through normalization
51                 // one can chop off the prefix, pass it to GetFullPath and add it again.
52                 return path;
53             }
54
55             return PathHelper.Normalize(path);
56         }
57
58         public static string GetFullPath(string path, string basePath)
59         {
60             if (path == null)
61                 throw new ArgumentNullException(nameof(path));
62
63             if (basePath == null)
64                 throw new ArgumentNullException(nameof(basePath));
65
66             if (!IsPathFullyQualified(basePath))
67                 throw new ArgumentException(SR.Arg_BasePathNotFullyQualified, nameof(basePath));
68
69             if (basePath.Contains('\0') || path.Contains('\0'))
70                 throw new ArgumentException(SR.Argument_InvalidPathChars);
71
72             if (IsPathFullyQualified(path))
73                 return GetFullPath(path);
74
75             if (PathInternal.IsEffectivelyEmpty(path))
76                 return basePath;
77
78             int length = path.Length;
79             string combinedPath = null;
80
81             if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])))
82             {
83                 // Path is current drive rooted i.e. starts with \:
84                 // "\Foo" and "C:\Bar" => "C:\Foo"
85                 // "\Foo" and "\\?\C:\Bar" => "\\?\C:\Foo"
86                 combinedPath = Join(GetPathRoot(basePath.AsSpan()), path.AsSpan(1)); // Cut the separator to ensure we don't end up with two separators when joining with the root.
87             }
88             else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar)
89             {
90                 // Drive relative paths
91                 Debug.Assert(length == 2 || !PathInternal.IsDirectorySeparator(path[2]));
92
93                 if (GetVolumeName(path).EqualsOrdinal(GetVolumeName(basePath)))
94                 {
95                     // Matching root
96                     // "C:Foo" and "C:\Bar" => "C:\Bar\Foo"
97                     // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
98                     combinedPath = Join(basePath, path.AsSpan(2));
99                 }
100                 else
101                 {
102                     // No matching root, root to specified drive
103                     // "D:Foo" and "C:\Bar" => "D:Foo"
104                     // "D:Foo" and "\\?\C:\Bar" => "\\?\D:\Foo"
105                     combinedPath = !PathInternal.IsDevice(basePath)
106                         ? path.Insert(2, @"\")
107                         : length == 2
108                             ? JoinInternal(basePath.AsSpan(0, 4), path, @"\")
109                             : JoinInternal(basePath.AsSpan(0, 4), path.AsSpan(0, 2), @"\", path.AsSpan(2));
110                 }
111             }
112             else
113             {
114                 // "Simple" relative path
115                 // "Foo" and "C:\Bar" => "C:\Bar\Foo"
116                 // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
117                 combinedPath = JoinInternal(basePath, path);
118             }
119
120             // Device paths are normalized by definition, so passing something of this format
121             // to GetFullPath() won't do anything by design. Additionally, GetFullPathName() in
122             // Windows doesn't root them properly. As such we need to manually remove segments.
123             return PathInternal.IsDevice(combinedPath)
124                 // Paths at this point are in the form of \\?\C:\.\tmp we skip to the last character of the root when calling RemoveRelativeSegments to remove relative paths in such cases.
125                 ? PathInternal.RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath) - 1)
126                 : GetFullPath(combinedPath);
127         }
128
129         public static string GetTempPath()
130         {
131             StringBuilder sb = StringBuilderCache.Acquire(Interop.Kernel32.MAX_PATH);
132             uint r = Interop.Kernel32.GetTempPathW(Interop.Kernel32.MAX_PATH, sb);
133             if (r == 0)
134                 throw Win32Marshal.GetExceptionForLastWin32Error();
135             return GetFullPath(StringBuilderCache.GetStringAndRelease(sb));
136         }
137
138         // Returns a unique temporary file name, and creates a 0-byte file by that
139         // name on disk.
140         public static string GetTempFileName()
141         {
142             string path = GetTempPath();
143
144             StringBuilder sb = StringBuilderCache.Acquire(Interop.Kernel32.MAX_PATH);
145             uint r = Interop.Kernel32.GetTempFileNameW(path, "tmp", 0, sb);
146             if (r == 0)
147                 throw Win32Marshal.GetExceptionForLastWin32Error();
148             return StringBuilderCache.GetStringAndRelease(sb);
149         }
150
151         // Tests if the given path contains a root. A path is considered rooted
152         // if it starts with a backslash ("\") or a valid drive letter and a colon (":").
153         public static bool IsPathRooted(string path)
154         {
155             return path != null && IsPathRooted(path.AsSpan());
156         }
157
158         public static bool IsPathRooted(ReadOnlySpan<char> path)
159         {
160             int length = path.Length;
161             return (length >= 1 && PathInternal.IsDirectorySeparator(path[0]))
162                 || (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar);
163         }
164
165         // Returns the root portion of the given path. The resulting string
166         // consists of those rightmost characters of the path that constitute the
167         // root of the path. Possible patterns for the resulting string are: An
168         // empty string (a relative path on the current drive), "\" (an absolute
169         // path on the current drive), "X:" (a relative path on a given drive,
170         // where X is the drive letter), "X:\" (an absolute path on a given drive),
171         // and "\\server\share" (a UNC path for a given server and share name).
172         // The resulting string is null if path is null. If the path is empty or
173         // only contains whitespace characters an ArgumentException gets thrown.
174         public static string GetPathRoot(string path)
175         {
176             if (PathInternal.IsEffectivelyEmpty(path))
177                 return null;
178
179             ReadOnlySpan<char> result = GetPathRoot(path.AsSpan());
180             if (path.Length == result.Length)
181                 return PathInternal.NormalizeDirectorySeparators(path);
182
183             return PathInternal.NormalizeDirectorySeparators(new string(result));
184         }
185
186         /// <remarks>
187         /// Unlike the string overload, this method will not normalize directory separators.
188         /// </remarks>
189         public static ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
190         {
191             if (PathInternal.IsEffectivelyEmpty(path))
192                 return ReadOnlySpan<char>.Empty;
193
194             int pathRoot = PathInternal.GetRootLength(path);
195             return pathRoot <= 0 ? ReadOnlySpan<char>.Empty : path.Slice(0, pathRoot);
196         }
197
198         /// <summary>Gets whether the system is case-sensitive.</summary>
199         internal static bool IsCaseSensitive { get { return false; } }
200
201         /// <summary>
202         /// Returns the volume name for dos, UNC and device paths.
203         /// </summary>
204         internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path)
205         {
206             // 3 cases: UNC ("\\server\share"), Device ("\\?\C:\"), or Dos ("C:\")
207             ReadOnlySpan<char> root = GetPathRoot(path);
208             if (root.Length == 0)
209                 return root;
210
211             int offset = GetUncRootLength(path);
212             if (offset >= 0)
213             {
214                 // Cut from "\\?\UNC\Server\Share" to "Server\Share"
215                 // Cut from  "\\Server\Share" to "Server\Share"
216                 return TrimEndingDirectorySeparator(root.Slice(offset));
217             }
218             else if (PathInternal.IsDevice(path))
219             {
220                 return TrimEndingDirectorySeparator(root.Slice(4)); // Cut from "\\?\C:\" to "C:"
221             }
222
223             return TrimEndingDirectorySeparator(root); // e.g. "C:"
224         }
225
226         /// <summary>
227         /// Trims the ending directory separator if present.
228         /// </summary>
229         /// <param name="path"></param>
230         internal static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<char> path) =>
231             PathInternal.EndsInDirectorySeparator(path) ?
232                 path.Slice(0, path.Length - 1) :
233                 path;
234
235         /// <summary>
236         /// Returns offset as -1 if the path is not in Unc format, otherwise returns the root length.
237         /// </summary>
238         /// <param name="path"></param>
239         /// <returns></returns>
240         internal static int GetUncRootLength(ReadOnlySpan<char> path)
241         {
242             bool isDevice = PathInternal.IsDevice(path);
243
244             if (!isDevice && path.Slice(0, 2).EqualsOrdinal(@"\\") )
245                 return 2;
246             else if (isDevice && path.Length >= 8
247                 && (path.Slice(0, 8).EqualsOrdinal(PathInternal.UncExtendedPathPrefix)
248                 || path.Slice(5, 4).EqualsOrdinal(@"UNC\")))
249                 return 8;
250
251             return -1;
252         }
253     }
254 }