2008-11-21 Marek Habersack <mhabersack@novell.com>
[mono.git] / mcs / class / System.Web / System.Web / VirtualPathUtility.cs
1 //
2 // System.Web.VirtualPathUtility.cs
3 //
4 // Author:
5 //      Chris Toshok (toshok@ximian.com)
6 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 //
8
9 //
10 // Copyright (C) 2005 Novell, Inc (http://www.novell.com)
11 //
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 //
31
32
33 using System.Web.Util;
34 using System.Text;
35
36 namespace System.Web {
37
38 #if NET_2_0
39         public
40 #endif
41         static class VirtualPathUtility
42         {
43                 public static string AppendTrailingSlash (string virtualPath)
44                 {
45                         if (virtualPath == null)
46                                 return virtualPath;
47
48                         int length = virtualPath.Length;
49                         if (length == 0 || virtualPath [length - 1] == '/')
50                                 return virtualPath;
51
52                         return virtualPath + "/";
53                 }
54
55                 public static string Combine (string basePath, string relativePath)
56                 {
57                         basePath = Normalize (basePath);
58
59                         if (IsRooted (relativePath))
60                                 return Normalize (relativePath);
61
62                         if (basePath [basePath.Length - 1] != '/') {
63                                 if (basePath.Length > 1) {
64                                         int lastSlash = basePath.LastIndexOf ('/');
65                                         if (lastSlash >= 0)
66                                                 basePath = basePath.Substring (0, lastSlash + 1);
67                                 }
68                                 else { // "~" only
69                                         basePath += "/";
70                                 }
71                         }
72
73                         return Normalize (basePath + relativePath);
74                 }
75
76                 public static string GetDirectory (string virtualPath)
77                 {
78                         return GetDirectory (virtualPath, true);
79                 }
80
81                 internal static string GetDirectory (string virtualPath, bool normalize)
82                 {
83                         if (normalize)
84                                 virtualPath = Normalize (virtualPath);
85
86                         if (IsAppRelative (virtualPath) && virtualPath.Length < 3) { // "~" or "~/"
87                                 virtualPath = ToAbsolute (virtualPath);
88                         }
89                         
90                         if (virtualPath.Length == 1 && virtualPath [0] == '/') { // "/"
91                                 return null;
92                         }
93
94                         int last = virtualPath.LastIndexOf ('/', virtualPath.Length - 2, virtualPath.Length - 2);
95                         if (last > 0)
96                                 return virtualPath.Substring (0, last + 1);
97                         else
98                                 return "/";
99                 }
100
101                 public static string GetExtension (string virtualPath)
102                 {
103                         if (StrUtils.IsNullOrEmpty (virtualPath))
104                                 throw new ArgumentNullException ("virtualPath");
105
106                         virtualPath = Canonize (virtualPath);
107
108                         int dot = virtualPath.LastIndexOf ('.');
109                         if (dot == -1 || dot == virtualPath.Length - 1 || dot < virtualPath.LastIndexOf ('/'))
110                                 return String.Empty;
111
112                         return virtualPath.Substring (dot);
113                 }
114
115                 public static string GetFileName (string virtualPath)
116                 {
117                         virtualPath = Normalize (virtualPath);
118                         
119                         if (IsAppRelative (virtualPath) && virtualPath.Length < 3) { // "~" or "~/"
120                                 virtualPath = ToAbsolute (virtualPath);
121                         }
122
123                         if (virtualPath.Length == 1 && virtualPath [0] == '/') { // "/"
124                                 return String.Empty;
125                         }
126
127                         virtualPath = RemoveTrailingSlash (virtualPath);
128                         int last = virtualPath.LastIndexOf ('/');
129                         return virtualPath.Substring (last + 1);
130                 }
131
132                 internal static bool IsRooted (string virtualPath)
133                 {
134                         return IsAbsolute (virtualPath) || IsAppRelative (virtualPath);
135                 }
136
137                 public static bool IsAbsolute (string virtualPath)
138                 {
139                         if (StrUtils.IsNullOrEmpty (virtualPath))
140                                 throw new ArgumentNullException ("virtualPath");
141
142                         return (virtualPath [0] == '/' || virtualPath [0] == '\\');
143                 }
144
145                 public static bool IsAppRelative (string virtualPath)
146                 {
147                         if (StrUtils.IsNullOrEmpty (virtualPath))
148                                 throw new ArgumentNullException ("virtualPath");
149
150                         if (virtualPath.Length == 1 && virtualPath [0] == '~')
151                                 return true;
152
153                         if (virtualPath [0] == '~' && (virtualPath [1] == '/' || virtualPath [1] == '\\'))
154                                 return true;
155
156                         return false;
157                 }
158
159                 // MSDN: If the fromPath and toPath parameters are not rooted; that is, 
160                 // they do not equal the root operator (the tilde [~]), do not start with a tilde (~), 
161                 // such as a tilde and a slash mark (~/) or a tilde and a double backslash (~//), 
162                 // or do not start with a slash mark (/), an ArgumentException exception is thrown.
163                 public static string MakeRelative (string fromPath, string toPath)
164                 {
165                         if (fromPath == null || toPath == null)
166                                 throw new NullReferenceException (); // yeah!
167
168                         if (toPath == "")
169                                 return toPath;
170
171                         toPath = ToAbsoluteInternal (toPath);
172                         fromPath = ToAbsoluteInternal (fromPath);
173
174                         if (String.CompareOrdinal (fromPath, toPath) == 0 && fromPath [fromPath.Length - 1] == '/')
175                                 return "./";
176
177                         string [] toPath_parts = toPath.Split ('/');
178                         string [] fromPath_parts = fromPath.Split ('/');
179                         int dest = 1;
180                         while (toPath_parts [dest] == fromPath_parts [dest]) {
181                                 if (toPath_parts.Length == (dest + 1) || fromPath_parts.Length == (dest + 1)) {
182                                         break;
183                                 }
184                                 dest++;
185                         }
186                         StringBuilder res = new StringBuilder();
187                         for (int i = 1; i < fromPath_parts.Length - dest; i++) {
188                                 res.Append ("../");
189                         }
190                         for (int i = dest; i < toPath_parts.Length; i++) {
191                                 res.Append (toPath_parts [i]);
192                                 if (i < toPath_parts.Length - 1)
193                                         res.Append ('/');
194                         }
195                         return res.ToString ();
196                 }
197
198                 static string ToAbsoluteInternal (string virtualPath)
199                 {
200                         if (IsAppRelative (virtualPath))
201                                 return ToAbsolute (virtualPath, HttpRuntime.AppDomainAppVirtualPath);
202                         else if (IsAbsolute (virtualPath))
203                                 return Normalize (virtualPath);
204
205                         throw new ArgumentOutOfRangeException ("Specified argument was out of the range of valid values.");
206                 }
207
208                 public static string RemoveTrailingSlash (string virtualPath)
209                 {
210                         if (virtualPath == null || virtualPath == "")
211                                 return null;
212
213                         int last = virtualPath.Length - 1;
214                         if (last == 0 || virtualPath [last] != '/')
215                                 return virtualPath;
216
217                         return virtualPath.Substring (0, last);
218                 }
219
220                 public static string ToAbsolute (string virtualPath)
221                 {
222                         return ToAbsolute (virtualPath, true);
223                 }
224
225                 internal static string ToAbsolute (string virtualPath, bool normalize)
226                 {
227                         if (IsAbsolute (virtualPath)) {
228                                 if (normalize)
229                                         return Normalize (virtualPath);
230                                 else
231                                         return virtualPath;
232                         }
233
234                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
235                         if (apppath == null)
236                                 throw new HttpException ("The path to the application is not known");
237
238                         if (virtualPath.Length == 1 && virtualPath [0] == '~')
239                                 return apppath;
240
241                         return ToAbsolute (virtualPath,apppath);
242                 }
243
244                 // If virtualPath is: 
245                 // Absolute, the ToAbsolute method returns the virtual path with no changes.
246                 // Application relative, the ToAbsolute method adds applicationPath to the beginning of the virtual path.
247                 // Not rooted, the ToAbsolute method raises an ArgumentOutOfRangeException exception.
248                 public static string ToAbsolute (string virtualPath, string applicationPath)
249                 {
250                         return ToAbsolute (virtualPath, applicationPath, true);
251                 }
252
253                 public static string ToAbsolute (string virtualPath, string applicationPath, bool normalize)
254                 {
255                         if (StrUtils.IsNullOrEmpty (applicationPath))
256                                 throw new ArgumentNullException ("applicationPath");
257
258                         if (StrUtils.IsNullOrEmpty (virtualPath))
259                                 throw new ArgumentNullException ("virtualPath");
260
261                         if (IsAppRelative(virtualPath)) {
262                                 if (applicationPath [0] != '/')
263                                         throw new ArgumentException ("appPath is not rooted", "applicationPath");
264                                         
265                                 string path = applicationPath + (virtualPath.Length == 1 ? "/" : virtualPath.Substring (1));
266                                 if (normalize)
267                                         return Normalize (path);
268                                 else
269                                         return path;
270                         }
271
272                         if (virtualPath [0] != '/')
273                                 throw new ArgumentException (String.Format ("Relative path not allowed: '{0}'", virtualPath));
274
275                         if (normalize)
276                                 return Normalize (virtualPath);
277                         else
278                                 return virtualPath;
279
280                 }
281
282                 public static string ToAppRelative (string virtualPath)
283                 {
284                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
285                         if (apppath == null)
286                                 throw new HttpException ("The path to the application is not known");
287
288                         return ToAppRelative (virtualPath, apppath);
289                 }
290
291                 public static string ToAppRelative (string virtualPath, string applicationPath)
292                 {
293                         virtualPath = Normalize (virtualPath);
294                         if (IsAppRelative (virtualPath))
295                                 return virtualPath;
296
297                         if (!IsAbsolute (applicationPath))
298                                 throw new ArgumentException ("appPath is not absolute", "applicationPath");
299                         
300                         applicationPath = Normalize (applicationPath);
301
302                         if (applicationPath.Length == 1)
303                                 return "~" + virtualPath;
304
305                         int appPath_lenght = applicationPath.Length;
306                         if (String.CompareOrdinal (virtualPath, applicationPath) == 0)
307                                 return "~/";
308                         if (String.CompareOrdinal (virtualPath, 0, applicationPath, 0, appPath_lenght) == 0)
309                                 return "~" + virtualPath.Substring (appPath_lenght);
310
311                         return virtualPath;
312                 }
313
314                 static char [] path_sep = { '/' };
315
316                 static string Normalize (string path)
317                 {
318                         if (!IsRooted (path))
319                                 throw new ArgumentException (String.Format ("The relative virtual path '{0}' is not allowed here.", path));
320
321                         if (path.Length == 1) // '/' or '~'
322                                 return path;
323
324                         path = Canonize (path);
325
326                         int dotPos = path.IndexOf ('.');
327                         while (dotPos >= 0) {
328                                 if (++dotPos == path.Length)
329                                         break;
330
331                                 char nextChar = path [dotPos];
332
333                                 if ((nextChar == '/') || (nextChar == '.'))
334                                         break;
335
336                                 dotPos = path.IndexOf ('.', dotPos);
337                         }
338
339                         if (dotPos < 0)
340                                 return path;
341
342                         bool starts_with_tilda = false;
343                         bool ends_with_slash = false;
344                         string [] apppath_parts= null;
345
346                         if (path [0] == '~') {
347                                 if (path.Length == 2) // "~/"
348                                         return "~/";
349                                 starts_with_tilda = true;
350                                 path = path.Substring (1);
351                         }
352                         else if (path.Length == 1) { // "/"
353                                 return "/";
354                         }
355
356                         if (path [path.Length - 1] == '/')
357                                 ends_with_slash = true;
358
359                         string [] parts = StrUtils.SplitRemoveEmptyEntries (path, path_sep);
360                         int end = parts.Length;
361
362                         int dest = 0;
363
364                         for (int i = 0; i < end; i++) {
365                                 string current = parts [i];
366                                 if (current == ".")
367                                         continue;
368
369                                 if (current == "..") {
370                                         dest--;
371
372                                         if(dest >= 0)
373                                                 continue;
374
375                                         if (starts_with_tilda) {
376                                                 if (apppath_parts == null) {
377                                                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
378                                                         apppath_parts = StrUtils.SplitRemoveEmptyEntries (apppath, path_sep);
379                                                 }
380
381                                                 if ((apppath_parts.Length + dest) >= 0)
382                                                         continue;
383                                         }
384                                         
385                                         throw new HttpException ("Cannot use a leading .. to exit above the top directory.");
386                                 }
387
388                                 if (dest >= 0)
389                                         parts [dest] = current;
390                                 else
391                                         apppath_parts [apppath_parts.Length + dest] = current;
392                                 
393                                 dest++;
394                         }
395
396                         StringBuilder str = new StringBuilder();
397                         if (apppath_parts != null) {
398                                 starts_with_tilda = false;
399                                 int count = apppath_parts.Length;
400                                 if (dest < 0)
401                                         count += dest;
402                                 for (int i = 0; i < count; i++) {
403                                         str.Append ('/');
404                                         str.Append (apppath_parts [i]);
405                                 }
406                         }
407                         else if (starts_with_tilda) {
408                                 str.Append ('~');
409                         }
410
411                         for (int i = 0; i < dest; i++) {
412                                 str.Append ('/');
413                                 str.Append (parts [i]);
414                         }
415
416                         if (str.Length > 0) {
417                                 if (ends_with_slash)
418                                         str.Append ('/');
419                         }
420                         else {
421                                 return "/";
422                         }
423
424                         return str.ToString ();
425                 }
426
427                 internal static string Canonize (string path)
428                 {
429                         int index = -1;
430                         for (int i=0; i < path.Length; i++) {
431                                 if ((path [i] == '\\') || (path [i] == '/' && (i + 1) < path.Length && (path [i + 1] == '/' || path [i + 1] == '\\'))) {
432                                         index = i;
433                                         break;
434                                 }
435                         }
436                         if (index < 0)
437                                 return path;
438
439                         StringBuilder sb = new StringBuilder (path.Length);
440                         sb.Append (path, 0, index);
441
442                         for (int i = index; i < path.Length; i++) {
443                                 if (path [i] == '\\' || path [i] == '/') {
444                                         int next = i + 1;
445                                         if (next < path.Length && (path [next] == '\\' || path [next] == '/'))
446                                                 continue;
447                                         sb.Append ('/');
448                                 }
449                                 else {
450                                         sb.Append (path [i]);
451                                 }
452                         }
453
454                         return sb.ToString ();
455                 }
456
457                 // See: http://support.microsoft.com/kb/932552
458                 static readonly char[] invalidVirtualPathChars = {':', '*'};
459                 internal static bool IsValidVirtualPath (string path)
460                 {
461                         if (path == null)
462                                 return false;
463                         
464                         return path.IndexOfAny (invalidVirtualPathChars) == -1;
465                 }
466         }
467 }