2005-02-06 Gonzalo Paniagua Javier <gonzalo@ximian.com>
[mono.git] / mcs / class / corlib / System.IO / Path.cs
1 //------------------------------------------------------------------------------
2 // 
3 // System.IO.Path.cs 
4 //
5 // Copyright (C) 2001 Moonlight Enterprises, All Rights Reserved
6 // Copyright (C) 2002 Ximian, Inc. (http://www.ximian.com)
7 // Copyright (C) 2003 Ben Maurer
8 // 
9 // Author:         Jim Richardson, develop@wtfo-guru.com
10 //                 Dan Lewis (dihlewis@yahoo.co.uk)
11 //                 Gonzalo Paniagua Javier (gonzalo@ximian.com)
12 //                 Ben Maurer (bmaurer@users.sourceforge.net)
13 // Created:        Saturday, August 11, 2001 
14 //
15 //------------------------------------------------------------------------------
16
17 //
18 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
19 //
20 // Permission is hereby granted, free of charge, to any person obtaining
21 // a copy of this software and associated documentation files (the
22 // "Software"), to deal in the Software without restriction, including
23 // without limitation the rights to use, copy, modify, merge, publish,
24 // distribute, sublicense, and/or sell copies of the Software, and to
25 // permit persons to whom the Software is furnished to do so, subject to
26 // the following conditions:
27 // 
28 // The above copyright notice and this permission notice shall be
29 // included in all copies or substantial portions of the Software.
30 // 
31 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
34 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
35 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
36 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
37 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
38 //
39
40 using System;
41 using System.Runtime.CompilerServices;
42
43 namespace System.IO
44 {
45         public sealed class Path
46         {
47                 public static readonly char AltDirectorySeparatorChar;
48                 public static readonly char DirectorySeparatorChar;
49                 public static readonly char[] InvalidPathChars;
50                 public static readonly char PathSeparator;
51                 internal static readonly string DirectorySeparatorStr;
52                 public static readonly char VolumeSeparatorChar;
53
54                 private static readonly char[] PathSeparatorChars;
55                 private static readonly bool dirEqualsVolume;
56
57                 private Path ()
58                 {
59                 }
60
61                 // class methods
62                 public static string ChangeExtension (string path, string extension)
63                 {
64                         if (path == null)
65                                 return null;
66
67                         if (path.IndexOfAny (InvalidPathChars) != -1)
68                                 throw new ArgumentException ("Illegal characters in path", "path");
69
70                         int iExt = findExtension (path);
71
72                         if (extension == null)
73                                 return iExt < 0 ? path : path.Substring (0, iExt);
74                         else if (extension == String.Empty)
75                                 return iExt < 0 ? path + '.' : path.Substring (0, iExt + 1);
76
77                         else if (path.Length != 0) {
78                                 if (extension.Length > 0 && extension [0] != '.')
79                                         extension = "." + extension;
80                         } else
81                                 extension = String.Empty;
82                         
83                         if (iExt < 0) {
84                                 return path + extension;
85                         } else if (iExt > 0) {
86                                 string temp = path.Substring (0, iExt);
87                                 return temp + extension;
88                         }
89
90                         return extension;
91                 }
92
93                 public static string Combine (string path1, string path2)
94                 {
95                         if (path1 == null)
96                                 throw new ArgumentNullException ("path1");
97
98                         if (path2 == null)
99                                 throw new ArgumentNullException ("path2");
100
101                         if (path1 == String.Empty)
102                                 return path2;
103
104                         if (path2 == String.Empty)
105                                 return path1;
106
107                         if (path1.IndexOfAny (InvalidPathChars) != -1)
108                                 throw new ArgumentException ("Illegal characters in path", "path1");
109
110                         if (path2.IndexOfAny (InvalidPathChars) != -1)
111                                 throw new ArgumentException ("Illegal characters in path", "path2");
112
113                         //TODO???: UNC names
114                         // LAMESPEC: MS says that if path1 is not empty and path2 is a full path
115                         // it should throw ArgumentException
116                         if (IsPathRooted (path2))
117                                 return path2;
118                         
119                         char p1end = path1 [path1.Length - 1];
120                         if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
121                                 return path1 + DirectorySeparatorChar + path2;
122
123                         return path1 + path2;
124                 }
125
126                 public static string GetDirectoryName (string path)
127                 {
128                         // LAMESPEC: For empty string MS docs say both
129                         // return null AND throw exception.  Seems .NET throws.
130                         if (path == String.Empty)
131                                 throw new ArgumentException();
132
133                         if (path == null || GetPathRoot (path) == path)
134                                 return null;
135
136                         CheckArgument.WhitespaceOnly (path);
137                         CheckArgument.PathChars (path);
138
139                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
140                         if (nLast == 0)
141                                 nLast++;
142
143                         if (nLast > 0) {
144                                 string ret = path.Substring (0, nLast);
145                                 int l = ret.Length;
146                                 if (l >= 2 && ret [l - 1] == VolumeSeparatorChar)
147                                         return ret + DirectorySeparatorChar;
148                                 else
149                                         return ret;
150                         }
151
152                         return String.Empty;
153                 }
154
155                 public static string GetExtension (string path)
156                 {
157                         if (path == null)
158                                 return null;
159
160                         if (path.IndexOfAny (InvalidPathChars) != -1)
161                                 throw new ArgumentException ("Illegal characters in path", "path");
162
163                         int iExt = findExtension (path);
164
165                         if (iExt > -1)
166                         {
167                                 if (iExt < path.Length - 1)
168                                         return path.Substring (iExt);
169                         }
170                         return string.Empty;
171                 }
172
173                 public static string GetFileName (string path)
174                 {
175                         if (path == null || path == String.Empty)
176                                 return path;
177
178                         if (path.IndexOfAny (InvalidPathChars) != -1)
179                                 throw new ArgumentException ("Illegal characters in path", "path");
180
181                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
182                         if (nLast >= 0)
183                                 return path.Substring (nLast + 1);
184
185                         return path;
186                 }
187
188                 public static string GetFileNameWithoutExtension (string path)
189                 {
190                         return ChangeExtension (GetFileName (path), null);
191                 }
192
193                 public static string GetFullPath (string path)
194                 {
195                         if (path == null)
196                                 throw (new ArgumentNullException (
197                                         "path",
198                                         "You must specify a path when calling System.IO.Path.GetFullPath"));
199
200                         if (path.Trim () == String.Empty)
201                                 throw new ArgumentException ("The path is not of a legal form", "path");
202
203                         if (path.Length >= 2 &&
204                                 IsDsc (path [0]) &&
205                                 IsDsc (path [1])) {
206                                 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
207                                         throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
208                                 else
209                                         if (path [0] == DirectorySeparatorChar)
210                                                 return path; // UNC
211                                         else
212                                                 return path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
213                         }
214
215                         if (!IsPathRooted (path))
216                                 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
217                         else if (DirectorySeparatorChar == '\\' &&
218                                 path.Length >= 2 &&
219                                 IsDsc (path [0]) &&
220                                 !IsDsc (path [1])) { // like `\abc\def'
221                                 string current = Directory.GetCurrentDirectory ();
222                                 if (current [1] == VolumeSeparatorChar)
223                                         path = current.Substring (0, 2) + path;
224                                 else
225                                         path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
226                         }
227                         return CanonicalizePath (path);
228                 }
229
230                 static bool IsDsc (char c) {
231                         return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
232                 }
233                 
234                 public static string GetPathRoot (string path)
235                 {
236                         if (path == null)
237                                 return null;
238
239                         if (path == String.Empty)
240                                 throw new ArgumentException ("This specified path is invalid.");
241
242                         if (!IsPathRooted (path))
243                                 return String.Empty;
244                         
245                         if (DirectorySeparatorChar == '/') {
246                                 // UNIX
247                                 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
248                         } else {
249                                 // Windows
250                                 int len = 2;
251
252                                 if (path.Length == 1 && IsDsc (path [0]))
253                                         return DirectorySeparatorStr;
254                                 else if (path.Length < 2)
255                                         return String.Empty;
256
257                                 if (IsDsc (path [0]) && IsDsc (path[1])) {
258                                         // UNC: \\server or \\server\share
259                                         // Get server
260                                         while (len < path.Length && !IsDsc (path [len])) len++;
261                                         // Get share
262                                         while (len < path.Length && !IsDsc (path [len])) len++;
263                                         return DirectorySeparatorStr +
264                                                 DirectorySeparatorStr +
265                                                 path.Substring (2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
266                                 } else if (IsDsc (path [0])) {
267                                         // path starts with '\' or '/'
268                                         return DirectorySeparatorStr;
269                                 } else if (path[1] == VolumeSeparatorChar) {
270                                         // C:\folder
271                                         if (path.Length >= 3 && (IsDsc (path [2]))) len++;
272                                 } else
273                                         return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
274                                 return path.Substring (0, len);
275                         }
276                 }
277
278                 public static string GetTempFileName ()
279                 {
280                         FileStream f = null;
281                         string path;
282                         Random rnd;
283                         int num = 0;
284
285                         rnd = new Random ();
286                         do {
287                                 num = rnd.Next ();
288                                 num++;
289                                 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x"));
290
291                                 try {
292                                         f = new FileStream (path, FileMode.CreateNew);
293                                 } catch {
294                                 }
295                         } while (f == null);
296                         
297                         f.Close();
298                         return path;
299                 }
300
301                 /// <summary>
302                 /// Returns the path of the current systems temp directory
303                 /// </summary>
304                 public static string GetTempPath ()
305                 {
306                         string p = get_temp_path ();
307                         if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
308                                 return p + DirectorySeparatorChar;
309
310                         return p;
311                 }
312
313                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
314                 private static extern string get_temp_path ();
315
316                 public static bool HasExtension (string path)
317                 {  
318                         if (path == null || path.Trim () == String.Empty)
319                                 return false;
320
321                         int pos = findExtension (path);
322                         return 0 <= pos && pos < path.Length - 1;
323                 }
324
325                 public static bool IsPathRooted (string path)
326                 {
327                         if (path == null || path.Length == 0)
328                                 return false;
329
330                         if (path.IndexOfAny (InvalidPathChars) != -1)
331                                 throw new ArgumentException ("Illegal characters in path", "path");
332
333                         char c = path [0];
334                         return (c == DirectorySeparatorChar     ||
335                                 c == AltDirectorySeparatorChar  ||
336                                 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
337                 }
338
339                 // private class methods
340
341                 private static int findExtension (string path)
342                 {
343                         // method should return the index of the path extension
344                         // start or -1 if no valid extension
345                         if (path != null){
346                                 int iLastDot = path.LastIndexOf ('.');
347                                 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
348
349                                 if (iLastDot > iLastSep)
350                                         return iLastDot;
351                         }
352                         return -1;
353                 }
354
355                 static Path () {
356                         VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
357                         DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
358                         AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
359
360                         PathSeparator = MonoIO.PathSeparator;
361                         InvalidPathChars = MonoIO.InvalidPathChars;
362
363                         // internal fields
364
365                         DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
366                         PathSeparatorChars = new char [] {
367                                 DirectorySeparatorChar,
368                                 AltDirectorySeparatorChar,
369                                 VolumeSeparatorChar
370                         };
371
372                         dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
373                 }
374                 
375                 
376                 static string CanonicalizePath (string path) {
377                         
378                         // STEP 1: Check for empty string
379                         if (path == null) return path;
380                         path = path.Trim ();
381                         if (path == String.Empty) return path;
382                         
383                         // STEP 2: Check to see if this is only a root
384                         string root = GetPathRoot (path);
385                         // it will return '\' for path '\', while it should return 'c:\' or so.
386                         // Note: commenting this out makes the ened for the (target == 1...) check in step 5
387                         //if (root == path) return path;
388                                 
389                         // STEP 3: split the directories, this gets rid of consecutative "/"'s
390                         string [] dirs = path.Split (DirectorySeparatorChar, AltDirectorySeparatorChar);
391                         // STEP 4: Get rid of directories containing . and ..
392                         int target = 0;
393                         
394                         for (int i = 0; i < dirs.Length; i++) {
395                                 if (dirs [i] == "." || (i != 0 && dirs [i] == String.Empty)) continue;
396                                 else if (dirs [i] == "..") {
397                                         if (target != 0) target--;
398                                 }
399                                 else
400                                         dirs [target++] = dirs [i];
401                         }
402
403                         // STEP 5: Combine everything.
404                         if (target == 0 || (target == 1 && dirs [0] == ""))
405                                 return root;
406                         else {
407                                 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
408                                 switch (DirectorySeparatorChar) {
409                                 case '\\': // Windows
410                                         // In GetFullPath(), it is assured that here never comes UNC. So this must only applied to such path that starts with '\', without drive specification.
411                                         if (path [0] != DirectorySeparatorChar && path.StartsWith (root)) {
412                                                 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
413                                                         ret += DirectorySeparatorChar;
414                                                 return ret;
415                                         } else {
416                                                 string current = Directory.GetCurrentDirectory ();
417                                                 if (current.Length > 1 && current [1] == VolumeSeparatorChar) {
418                                                         // DOS local file path
419                                                         if (ret.Length == 0 || IsDsc (ret [0]))
420                                                                 ret += '\\';
421                                                         return current.Substring (0, 2) + ret;
422                                                 }
423                                                 else if (IsDsc (current [current.Length - 1]) && IsDsc (ret [0]))
424                                                         return current + ret.Substring (1);
425                                                 else
426                                                         return current + ret;
427                                         }
428                                 default: // Unix/Mac
429                                         return ret;
430                                 }
431                         }
432                 }
433         }
434 }