initial implementation of 'unify request'
[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                 private 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 #if DEBUG
294                         Console.WriteLine ("VirtualPathUtility.ToAppRelative (\"{0}\", \"{1}\")", virtualPath, applicationPath);
295 #endif
296                         virtualPath = Normalize (virtualPath);
297 #if DEBUG
298                         Console.WriteLine ("\tvirtualPath (normalized): {0}", virtualPath);
299 #endif
300                         if (IsAppRelative (virtualPath))
301                                 return virtualPath;
302
303                         if (!IsAbsolute (applicationPath))
304                                 throw new ArgumentException ("appPath is not absolute", "applicationPath");
305                         
306                         applicationPath = Normalize (applicationPath);
307
308                         if (applicationPath.Length == 1)
309                                 return "~" + virtualPath;
310
311                         int appPath_lenght = applicationPath.Length;
312                         if (String.CompareOrdinal (virtualPath, applicationPath) == 0)
313                                 return "~/";
314                         if (String.CompareOrdinal (virtualPath, 0, applicationPath, 0, appPath_lenght) == 0)
315                                 return "~" + virtualPath.Substring (appPath_lenght);
316
317                         return virtualPath;
318                 }
319
320                 static char [] path_sep = { '/' };
321
322                 static string Normalize (string path)
323                 {
324                         if (!IsRooted (path))
325                                 throw new ArgumentException (String.Format ("The relative virtual path '{0}' is not allowed here.", path));
326
327                         if (path.Length == 1) // '/' or '~'
328                                 return path;
329
330                         path = Canonize (path);
331
332                         int dotPos = path.IndexOf ('.');
333                         while (dotPos >= 0) {
334                                 if (++dotPos == path.Length)
335                                         break;
336
337                                 char nextChar = path [dotPos];
338
339                                 if ((nextChar == '/') || (nextChar == '.'))
340                                         break;
341
342                                 dotPos = path.IndexOf ('.', dotPos);
343                         }
344
345                         if (dotPos < 0)
346                                 return path;
347
348                         bool starts_with_tilda = false;
349                         bool ends_with_slash = false;
350                         string [] apppath_parts= null;
351
352                         if (path [0] == '~') {
353                                 if (path.Length == 2) // "~/"
354                                         return "~/";
355                                 starts_with_tilda = true;
356                                 path = path.Substring (1);
357                         }
358                         else if (path.Length == 1) { // "/"
359                                 return "/";
360                         }
361
362                         if (path [path.Length - 1] == '/')
363                                 ends_with_slash = true;
364
365                         string [] parts = StrUtils.SplitRemoveEmptyEntries (path, path_sep);
366                         int end = parts.Length;
367
368                         int dest = 0;
369
370                         for (int i = 0; i < end; i++) {
371                                 string current = parts [i];
372                                 if (current == ".")
373                                         continue;
374
375                                 if (current == "..") {
376                                         dest--;
377
378                                         if(dest >= 0)
379                                                 continue;
380
381                                         if (starts_with_tilda) {
382                                                 if (apppath_parts == null) {
383                                                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
384                                                         apppath_parts = StrUtils.SplitRemoveEmptyEntries (apppath, path_sep);
385                                                 }
386
387                                                 if ((apppath_parts.Length + dest) >= 0)
388                                                         continue;
389                                         }
390                                         
391                                         throw new HttpException ("Cannot use a leading .. to exit above the top directory.");
392                                 }
393
394                                 if (dest >= 0)
395                                         parts [dest] = current;
396                                 else
397                                         apppath_parts [apppath_parts.Length + dest] = current;
398                                 
399                                 dest++;
400                         }
401
402                         StringBuilder str = new StringBuilder();
403                         if (apppath_parts != null) {
404                                 starts_with_tilda = false;
405                                 int count = apppath_parts.Length;
406                                 if (dest < 0)
407                                         count += dest;
408                                 for (int i = 0; i < count; i++) {
409                                         str.Append ('/');
410                                         str.Append (apppath_parts [i]);
411                                 }
412                         }
413                         else if (starts_with_tilda) {
414                                 str.Append ('~');
415                         }
416
417                         for (int i = 0; i < dest; i++) {
418                                 str.Append ('/');
419                                 str.Append (parts [i]);
420                         }
421
422                         if (str.Length > 0) {
423                                 if (ends_with_slash)
424                                         str.Append ('/');
425                         }
426                         else {
427                                 return "/";
428                         }
429
430                         return str.ToString ();
431                 }
432
433                 static string Canonize (string path)
434                 {
435                         int index = -1;
436                         for (int i=0; i < path.Length; i++) {
437                                 if ((path [i] == '\\') || (path [i] == '/' && (i + 1) < path.Length && (path [i + 1] == '/' || path [i + 1] == '\\'))) {
438                                         index = i;
439                                         break;
440                                 }
441                         }
442                         if (index < 0)
443                                 return path;
444
445                         StringBuilder sb = new StringBuilder (path.Length);
446                         sb.Append (path, 0, index);
447
448                         for (int i = index; i < path.Length; i++) {
449                                 if (path [i] == '\\' || path [i] == '/') {
450                                         int next = i + 1;
451                                         if (next < path.Length && (path [next] == '\\' || path [next] == '/'))
452                                                 continue;
453                                         sb.Append ('/');
454                                 }
455                                 else {
456                                         sb.Append (path [i]);
457                                 }
458                         }
459
460                         return sb.ToString ();
461                 }
462
463         }
464 }