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