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.Runtime.InteropServices;
7 using System.Text;
8 
9 namespace System.IO
10 {
11     public static partial class Path
12     {
GetInvalidFileNameChars()13         public static char[] GetInvalidFileNameChars() => new char[] { '\0', '/' };
14 
GetInvalidPathChars()15         public static char[] GetInvalidPathChars() => new char[] { '\0' };
16 
17         // Expands the given path to a fully qualified path.
GetFullPath(string path)18         public static string GetFullPath(string path)
19         {
20             if (path == null)
21                 throw new ArgumentNullException(nameof(path));
22 
23             if (path.Length == 0)
24                 throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
25 
26             if (path.IndexOf('\0') != -1)
27                 throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
28 
29             // Expand with current directory if necessary
30             if (!IsPathRooted(path))
31             {
32                 path = Combine(Interop.Sys.GetCwd(), path);
33             }
34 
35             // We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist,
36             // and turns it into a full path, which we only want if fullCheck is true.
37             string collapsedString = RemoveRelativeSegments(path);
38 
39             Debug.Assert(collapsedString.Length < path.Length || collapsedString.ToString() == path,
40                 "Either we've removed characters, or the string should be unmodified from the input path.");
41 
42             string result = collapsedString.Length == 0 ? PathInternal.DirectorySeparatorCharAsString : collapsedString;
43 
44             return result;
45         }
46 
47         /// <summary>
48         /// Try to remove relative segments from the given path (without combining with a root).
49         /// </summary>
50         /// <param name="skip">Skip the specified number of characters before evaluating.</param>
RemoveRelativeSegments(string path, int skip = 0)51         private static string RemoveRelativeSegments(string path, int skip = 0)
52         {
53             bool flippedSeparator = false;
54 
55             // Remove "//", "/./", and "/../" from the path by copying each character to the output,
56             // except the ones we're removing, such that the builder contains the normalized path
57             // at the end.
58             var sb = StringBuilderCache.Acquire(path.Length);
59             if (skip > 0)
60             {
61                 sb.Append(path, 0, skip);
62             }
63 
64             for (int i = skip; i < path.Length; i++)
65             {
66                 char c = path[i];
67 
68                 if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length)
69                 {
70                     // Skip this character if it's a directory separator and if the next character is, too,
71                     // e.g. "parent//child" => "parent/child"
72                     if (PathInternal.IsDirectorySeparator(path[i + 1]))
73                     {
74                         continue;
75                     }
76 
77                     // Skip this character and the next if it's referring to the current directory,
78                     // e.g. "parent/./child" =? "parent/child"
79                     if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) &&
80                         path[i + 1] == '.')
81                     {
82                         i++;
83                         continue;
84                     }
85 
86                     // Skip this character and the next two if it's referring to the parent directory,
87                     // e.g. "parent/child/../grandchild" => "parent/grandchild"
88                     if (i + 2 < path.Length &&
89                         (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) &&
90                         path[i + 1] == '.' && path[i + 2] == '.')
91                     {
92                         // Unwind back to the last slash (and if there isn't one, clear out everything).
93                         int s;
94                         for (s = sb.Length - 1; s >= 0; s--)
95                         {
96                             if (PathInternal.IsDirectorySeparator(sb[s]))
97                             {
98                                 sb.Length = s;
99                                 break;
100                             }
101                         }
102                         if (s < 0)
103                         {
104                             sb.Length = 0;
105                         }
106 
107                         i += 2;
108                         continue;
109                     }
110                 }
111 
112                 // Normalize the directory separator if needed
113                 if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar)
114                 {
115                     c = PathInternal.DirectorySeparatorChar;
116                     flippedSeparator = true;
117                 }
118 
119                 sb.Append(c);
120             }
121 
122             if (flippedSeparator || sb.Length != path.Length)
123             {
124                 return StringBuilderCache.GetStringAndRelease(sb);
125             }
126             else
127             {
128                 // We haven't changed the source path, return the original
129                 StringBuilderCache.Release(sb);
130                 return path;
131             }
132         }
133 
RemoveLongPathPrefix(string path)134         private static string RemoveLongPathPrefix(string path)
135         {
136             return path; // nop.  There's nothing special about "long" paths on Unix.
137         }
138 
GetTempPath()139         public static string GetTempPath()
140         {
141             const string TempEnvVar = "TMPDIR";
142             const string DefaultTempPath = "/tmp/";
143 
144             // Get the temp path from the TMPDIR environment variable.
145             // If it's not set, just return the default path.
146             // If it is, return it, ensuring it ends with a slash.
147             string path = Environment.GetEnvironmentVariable(TempEnvVar);
148             return
149                 string.IsNullOrEmpty(path) ? DefaultTempPath :
150                 PathInternal.IsDirectorySeparator(path[path.Length - 1]) ? path :
151                 path + PathInternal.DirectorySeparatorChar;
152         }
153 
GetTempFileName()154         public static string GetTempFileName()
155         {
156             const string Suffix = ".tmp";
157             const int SuffixByteLength = 4;
158 
159             // mkstemps takes a char* and overwrites the XXXXXX with six characters
160             // that'll result in a unique file name.
161             string template = GetTempPath() + "tmpXXXXXX" + Suffix + "\0";
162             byte[] name = Encoding.UTF8.GetBytes(template);
163 
164             // Create, open, and close the temp file.
165             IntPtr fd = Interop.CheckIo(Interop.Sys.MksTemps(name, SuffixByteLength));
166             Interop.Sys.Close(fd); // ignore any errors from close; nothing to do if cleanup isn't possible
167 
168             // 'name' is now the name of the file
169             Debug.Assert(name[name.Length - 1] == '\0');
170             return Encoding.UTF8.GetString(name, 0, name.Length - 1); // trim off the trailing '\0'
171         }
172 
IsPathRooted(string path)173         public static bool IsPathRooted(string path)
174         {
175             if (path == null)
176                 return false;
177 
178             return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar;
179         }
180 
181         // The resulting string is null if path is null. If the path is empty or
182         // only contains whitespace characters an ArgumentException gets thrown.
GetPathRoot(string path)183         public static string GetPathRoot(string path)
184         {
185             if (path == null) return null;
186             if (PathInternal.IsEffectivelyEmpty(path))
187                 throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));
188 
189             return IsPathRooted(path) ? PathInternal.DirectorySeparatorCharAsString : String.Empty;
190         }
191 
192         /// <summary>Gets whether the system is case-sensitive.</summary>
193         internal static bool IsCaseSensitive
194         {
195             get
196             {
197                 #if PLATFORM_OSX
198                     return false;
199                 #else
200                     return true;
201                 #endif
202             }
203         }
204     }
205 }
206