Merge pull request #2250 from esdrubal/master
[mono.git] / mcs / class / referencesource / System.Web / Util / FileUtil.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="UrlPath.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6
7 /*
8  * UrlPath class
9  *
10  * Copyright (c) 1999 Microsoft Corporation
11  */
12
13 namespace System.Web.Util {
14 using System.Security.Permissions;
15 using System.Text;
16 using System.Runtime.Serialization.Formatters;
17 using System.Runtime.InteropServices;
18 using System.Collections;
19 using System.Globalization;
20 using System.IO;
21 using System.Web.Hosting;
22
23
24 internal struct FileTimeInfo {
25     internal long LastWriteTime;
26     internal long Size;
27
28     internal static readonly FileTimeInfo MinValue = new FileTimeInfo(0, 0);
29
30     internal FileTimeInfo(long lastWriteTime, long size) {
31         LastWriteTime = lastWriteTime;
32         Size = size;
33     }
34
35     public override bool Equals(object obj) {
36         FileTimeInfo fti;
37
38         if (obj is FileTimeInfo) {
39             fti = (FileTimeInfo) obj;
40             return (LastWriteTime == fti.LastWriteTime) && (Size == fti.Size);
41         }
42         else {
43             return false;
44         }
45     }
46
47     public static bool operator == (FileTimeInfo value1, FileTimeInfo value2) 
48     {
49         return (value1.LastWriteTime == value2.LastWriteTime) &&
50                (value1.Size == value2.Size);
51     }
52
53     public unsafe static bool operator != (FileTimeInfo value1, FileTimeInfo value2) 
54     {
55         return !(value1 == value2);
56     }
57
58     public override int GetHashCode(){
59         return HashCodeCombiner.CombineHashCodes(LastWriteTime.GetHashCode(), Size.GetHashCode());
60     }
61
62     
63 }
64
65 /*
66  * Helper methods relating to file operations
67  */
68 internal class FileUtil {
69
70     private FileUtil() {
71     }
72
73     [FileIOPermission(SecurityAction.Assert, AllFiles = FileIOPermissionAccess.Read)]
74     internal static bool FileExists(String filename) {
75         bool exists = false;
76
77         try {
78             exists = File.Exists(filename);
79         }
80         catch {
81         }
82
83         return exists;
84     }
85
86     // For a given path, if its beneath the app root, return the first existing directory
87     internal static string GetFirstExistingDirectory(string appRoot, string fileName) {
88         if (IsBeneathAppRoot(appRoot, fileName)) {
89             string existingDir = appRoot;
90             do {
91                 int nextSeparator = fileName.IndexOf(Path.DirectorySeparatorChar, existingDir.Length + 1);
92                 if (nextSeparator > -1) {
93                     string nextDir = fileName.Substring(0, nextSeparator);
94                     if (FileUtil.DirectoryExists(nextDir, false)) {
95                         existingDir = nextDir;
96                         continue;
97                     }
98                 }
99                 break;
100             } while (true);
101
102             return existingDir;
103         }
104         return null;
105     }
106
107     internal static bool IsBeneathAppRoot(string appRoot, string filePath) {
108         if (filePath.Length > appRoot.Length + 1
109             && filePath.IndexOf(appRoot, StringComparison.OrdinalIgnoreCase) > -1
110             && filePath[appRoot.Length] == Path.DirectorySeparatorChar) {
111             return true;
112         }
113         return false;
114     }
115
116     // Remove the final backslash from a directory path, unless it's something like c:\
117     internal static String RemoveTrailingDirectoryBackSlash(String path) {
118
119         if (path == null)
120             return null;
121
122         int length = path.Length;
123         if (length > 3 && path[length - 1] == '\\')
124             path = path.Substring(0, length - 1);
125
126         return path;
127     }
128
129     private static int _maxPathLength = 259;
130     // If the path is longer than the maximum length
131     // Trim the end and append the hashcode to it.
132     internal static String TruncatePathIfNeeded(string path, int reservedLength) {
133         int maxPathLength = _maxPathLength - reservedLength;
134         if (path.Length > maxPathLength) {
135             // 
136
137             path = path.Substring(0, maxPathLength - 13) +
138                 path.GetHashCode().ToString(CultureInfo.InvariantCulture);
139         }
140
141         return path;
142     }
143
144     /*
145      * Canonicalize the directory, and makes sure it ends with a '\'
146      */
147     internal static string FixUpPhysicalDirectory(string dir) {
148         if (dir == null)
149             return null;
150
151         dir = Path.GetFullPath(dir);
152
153         // Append '\' to the directory if necessary.
154         if (!StringUtil.StringEndsWith(dir, @"\"))
155             dir = dir + @"\";
156
157         return dir;
158     }
159
160     // Fail if the physical path is not canonical
161     static internal void CheckSuspiciousPhysicalPath(string physicalPath) {
162         if (IsSuspiciousPhysicalPath(physicalPath)) {
163             throw new HttpException(404, String.Empty);
164         }
165     }
166
167     // Check whether the physical path is not canonical
168     // NOTE: this API throws if we don't have permission to the file.
169     // NOTE: The compare needs to be case insensitive (VSWhidbey 444513)
170     static internal bool IsSuspiciousPhysicalPath(string physicalPath) {
171         bool pathTooLong;
172
173         if (!IsSuspiciousPhysicalPath(physicalPath, out pathTooLong)) {
174             return false;
175         }
176
177         if (!pathTooLong) {
178             return true;
179         }
180
181         // physical path too long -> not good because we still need to make
182         // it work for virtual path provider scenarios
183
184         // first a few simple checks:
185         if (physicalPath.IndexOf('/') >= 0) {
186             return true;
187         }
188         
189         string slashDots = "\\..";
190         int idxSlashDots = physicalPath.IndexOf(slashDots, StringComparison.Ordinal);
191         if (idxSlashDots >= 0
192             && (physicalPath.Length == idxSlashDots + slashDots.Length
193                 || physicalPath[idxSlashDots + slashDots.Length] == '\\')) {
194             return true;
195         }
196
197         // the real check is to go right to left until there is no longer path-too-long
198         // and see if the canonicalization check fails then
199
200         int pos = physicalPath.LastIndexOf('\\');
201
202         while (pos >= 0) {
203             string path = physicalPath.Substring(0, pos);
204
205             if (!IsSuspiciousPhysicalPath(path, out pathTooLong)) {
206                 // reached a non-suspicious path that is not too long
207                 return false;
208             }
209
210             if (!pathTooLong) {
211                 // reached a suspicious path that is not too long
212                 return true;
213             }
214
215             // trim the path some more
216             pos = physicalPath.LastIndexOf('\\', pos-1);
217         }
218
219         // backtracted to the end without reaching a non-suspicious path
220         // this is suspicious (should happen because app root at least should be ok)
221         return true;
222     }
223
224     private static readonly char[] s_invalidPathChars = Path.GetInvalidPathChars();
225
226     // VSWhidbey 609102 - Medium trust apps may hit this method, and if the physical path exists,
227     // Path.GetFullPath will seek PathDiscovery permissions and throw an exception.
228     [FileIOPermissionAttribute(SecurityAction.Assert, AllFiles=FileIOPermissionAccess.PathDiscovery)]
229     static internal bool IsSuspiciousPhysicalPath(string physicalPath, out bool pathTooLong) {
230         bool isSuspicious;
231
232         // DevDiv 340712: GetConfigPathData generates n^2 exceptions where n is number of incorrectly placed '/'
233         // Explicitly prevent frequent exception cases since this method is called a few times per url segment
234         if ((physicalPath != null) &&
235              (physicalPath.Length > _maxPathLength ||
236              physicalPath.IndexOfAny(s_invalidPathChars) != -1 ||
237              // Contains ':' at any position other than 2nd char
238              (physicalPath.Length > 0 && physicalPath[0] == ':') ||
239              (physicalPath.Length > 2 && physicalPath.IndexOf(':', 2) > 0))) {
240
241             // see comment below
242             pathTooLong = true;
243             return true;
244         }
245
246         try {
247             isSuspicious = !String.IsNullOrEmpty(physicalPath) &&
248                 String.Compare(physicalPath, Path.GetFullPath(physicalPath),
249                     StringComparison.OrdinalIgnoreCase) != 0;
250             pathTooLong = false;
251         }
252         catch (PathTooLongException) {
253             isSuspicious = true;
254             pathTooLong = true;
255         }
256         catch (NotSupportedException) {
257             // see comment below -- we do the same for ':'
258             isSuspicious = true;
259             pathTooLong = true;
260         }
261         catch (ArgumentException) {
262             // DevDiv Bugs 152256:  Illegal characters {",|} in path prevent configuration system from working.
263             // We need to catch this exception and conservatively assume that the path is suspicious in 
264             // such a case.
265             // We also set pathTooLong to true because at this point we do not know if the path is too long
266             // or not. If we assume that pathTooLong is false, it means that our path length enforcement
267             // is bypassed by using URLs with illegal characters. We do not want that. Moreover, returning 
268             // pathTooLong = true causes the current logic to peel of URL fragments, which can also find a 
269             // path without illegal characters to retrieve the config.
270             isSuspicious = true;
271             pathTooLong = true;
272         }
273
274         return isSuspicious;
275     }
276
277     static bool HasInvalidLastChar(string physicalPath) {
278         // see VSWhidbey #108945
279         // We need to filter out directory names which end
280         // in " " or ".".  We want to treat path names that 
281         // end in these characters as files - however, Windows
282         // will strip these characters off the end of the name,
283         // which may result in the name being treated as a 
284         // directory instead.
285
286         if (String.IsNullOrEmpty(physicalPath)) {
287             return false;
288         }
289         
290         char lastChar = physicalPath[physicalPath.Length - 1];
291         return lastChar == ' ' || lastChar == '.';
292     }
293
294     internal static bool DirectoryExists(String dirname) {
295         bool exists = false;
296         dirname = RemoveTrailingDirectoryBackSlash(dirname);
297         if (HasInvalidLastChar(dirname))
298             return false;
299
300         try {
301             exists = Directory.Exists(dirname);
302         }
303         catch {
304         }
305
306         return exists;
307     }
308
309     internal static bool DirectoryAccessible(String dirname) {
310         bool accessible = false;
311         dirname = RemoveTrailingDirectoryBackSlash(dirname);
312         if (HasInvalidLastChar(dirname))
313             return false;
314
315         try {
316             accessible = (new DirectoryInfo(dirname)).Exists;
317         }
318         catch {
319         }
320
321         return accessible;
322     }
323
324     private static Char[] _invalidFileNameChars = Path.GetInvalidFileNameChars();
325     internal static bool IsValidDirectoryName(String name) {
326         if (String.IsNullOrEmpty(name)) {
327             return false;
328         }
329
330         if (name.IndexOfAny(_invalidFileNameChars, 0) != -1) {
331             return false;
332         }
333
334         if (name.Equals(".") || name.Equals("..")) {
335             return false;
336         }
337
338         return true;
339     }
340
341     //
342     // Given a physical path, determine if it exists, and whether it is a directory or file.
343     //
344     // If directoryExistsOnError is set, set exists=true and isDirectory=true if we cannot confirm that the path does not exist.
345     // If fileExistsOnError is set, set exists=true and isDirectory=false if we cannot confirm that the path does not exist.
346     //
347
348     // this code is called by config that doesn't have AspNetHostingPermission
349     internal static void PhysicalPathStatus(string physicalPath, bool directoryExistsOnError, bool fileExistsOnError, out bool exists, out bool isDirectory) {
350         exists = false;
351         isDirectory = true;
352
353         Debug.Assert(!(directoryExistsOnError && fileExistsOnError), "!(directoryExistsOnError && fileExistsOnError)");
354
355         if (String.IsNullOrEmpty(physicalPath))
356             return;
357
358         using (new ApplicationImpersonationContext()) {
359             UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data;
360             bool ok = UnsafeNativeMethods.GetFileAttributesEx(physicalPath, UnsafeNativeMethods.GetFileExInfoStandard, out data);
361             if (ok) {
362                 exists = true;
363                 isDirectory = ((data.fileAttributes & (int) FileAttributes.Directory) == (int) FileAttributes.Directory);
364                 if (isDirectory && HasInvalidLastChar(physicalPath)) {
365                     exists = false;
366                 }
367             }
368             else {
369                 if (directoryExistsOnError || fileExistsOnError) {
370                     // Set exists to true if we cannot confirm that the path does NOT exist.
371                     int hr = Marshal.GetHRForLastWin32Error();
372                     if (!(hr == HResults.E_FILENOTFOUND || hr == HResults.E_PATHNOTFOUND)) {
373                         exists = true;
374                         isDirectory = directoryExistsOnError;
375                     }
376                 }
377             }
378         }
379     }
380
381     //
382     // Use to avoid the perf hit of a Demand when the Demand is not necessary for security.
383     //
384     // If trueOnError is set, then return true if we cannot confirm that the file does NOT exist.
385     //
386     internal static bool DirectoryExists(string filename, bool trueOnError) {
387         filename = RemoveTrailingDirectoryBackSlash(filename);
388         if (HasInvalidLastChar(filename)) {
389             return false;
390         }
391
392         UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data;
393         bool ok = UnsafeNativeMethods.GetFileAttributesEx(filename, UnsafeNativeMethods.GetFileExInfoStandard, out data);
394         if (ok) {
395             // The path exists. Return true if it is a directory, false if a file.
396             return (data.fileAttributes & (int) FileAttributes.Directory) == (int) FileAttributes.Directory;
397         }
398         else {
399             if (!trueOnError) {
400                 return false;
401             }
402             else {
403                 // Return true if we cannot confirm that the file does NOT exist.
404                 int hr = Marshal.GetHRForLastWin32Error();
405                 if (hr == HResults.E_FILENOTFOUND || hr == HResults.E_PATHNOTFOUND) {
406                     return false;
407                 }
408                 else {
409                     return true;
410                 }
411             }
412         }
413     }
414 }
415
416
417 //
418 // Wraps the Win32 API FindFirstFile
419 //
420 sealed class FindFileData {
421
422     private FileAttributesData _fileAttributesData;
423     private string _fileNameLong;
424     private string _fileNameShort;
425
426     internal string FileNameLong { get { return _fileNameLong; } }
427     internal string FileNameShort { get { return _fileNameShort; } }
428     internal FileAttributesData FileAttributesData { get { return _fileAttributesData; } }
429
430     // FindFile - given a file name, gets the file attributes and short form (8.3 format) of a file name.
431     static internal int FindFile(string path, out FindFileData data) {
432         IntPtr hFindFile;
433         UnsafeNativeMethods.WIN32_FIND_DATA wfd;
434
435         data = null;
436
437         // Remove trailing slash if any, otherwise FindFirstFile won't work correctly
438         path = FileUtil.RemoveTrailingDirectoryBackSlash(path);
439 #if DBG
440         Debug.Assert(Path.GetDirectoryName(path) != null, "Path.GetDirectoryName(path) != null");
441         Debug.Assert(Path.GetFileName(path) != null, "Path.GetFileName(path) != null");
442 #endif
443
444         hFindFile = UnsafeNativeMethods.FindFirstFile(path, out wfd);
445         int lastError = Marshal.GetLastWin32Error(); // FXCOP demands that this preceed the == 
446         if (hFindFile == UnsafeNativeMethods.INVALID_HANDLE_VALUE) {
447             return HttpException.HResultFromLastError(lastError);
448         }
449
450         UnsafeNativeMethods.FindClose(hFindFile);
451
452 #if DBG
453         string file = Path.GetFileName(path);
454         file = file.TrimEnd(' ', '.');
455         Debug.Assert(StringUtil.EqualsIgnoreCase(file, wfd.cFileName) ||
456                      StringUtil.EqualsIgnoreCase(file, wfd.cAlternateFileName),
457                      "Path to FindFile is not for a single file: " + path);
458 #endif
459
460         data = new FindFileData(ref wfd);
461         return HResults.S_OK;
462     }
463
464     // FindFile - takes a full-path and a root-directory-path, and is used to get the
465     // short form (8.3 format) of the relative-path.  A FindFileData structure is returned
466     // with FileNameLong and FileNameShort relative to the specified root-directory-path.
467     //
468     // For example, if full-path is "c:\vdir\subdirectory\t.aspx" and root-directory-path 
469     // is "c:\vdir", then the relative-path will be "subdirectory\t.aspx" and it's short 
470     // form will be something like "subdir~1\t~1.ASP".
471     //
472     // This is used by FileChangesMonitor to support the ability to monitor all files and 
473     // directories at any depth beneath the application root directory. 
474     internal static int FindFile(string fullPath, string rootDirectoryPath, out FindFileData data) {
475
476         int hr = FindFileData.FindFile(fullPath, out data);
477         if (hr != HResults.S_OK || String.IsNullOrEmpty(rootDirectoryPath)) {
478             return hr;
479         }
480         
481 #if DBG
482         // The trailing slash should have been removed already, unless the root is "c:\"
483         Debug.Assert(rootDirectoryPath.Length < 4 || rootDirectoryPath[rootDirectoryPath.Length-1] != '\\', "Trailing slash unexpected: " + rootDirectoryPath);
484 #endif
485         
486         // remove it just in case
487         rootDirectoryPath = FileUtil.RemoveTrailingDirectoryBackSlash(rootDirectoryPath);
488        
489 #if DBG 
490         Debug.Assert(fullPath.IndexOf(rootDirectoryPath, StringComparison.OrdinalIgnoreCase) == 0, 
491                      "fullPath (" + fullPath + ") is not within rootDirectoryPath (" + rootDirectoryPath + ")");
492 #endif
493         
494         // crawl backwards along the subdirectories of fullPath until we get to the specified rootDirectoryPath
495         string relativePathLong = String.Empty;
496         string relativePathShort = String.Empty;
497         string currentParentDir = Path.GetDirectoryName(fullPath);
498         while (currentParentDir != null 
499                && currentParentDir.Length > rootDirectoryPath.Length+1 
500                && currentParentDir.IndexOf(rootDirectoryPath, StringComparison.OrdinalIgnoreCase) == 0) {
501             
502             UnsafeNativeMethods.WIN32_FIND_DATA fd;
503             IntPtr hFindFile = UnsafeNativeMethods.FindFirstFile(currentParentDir, out fd);
504             int lastError = Marshal.GetLastWin32Error(); // FXCOP demands that this preceed the == 
505             if (hFindFile == UnsafeNativeMethods.INVALID_HANDLE_VALUE) {
506                 return HttpException.HResultFromLastError(lastError);
507             }
508             UnsafeNativeMethods.FindClose(hFindFile);
509             
510 #if DBG
511             Debug.Assert(!String.IsNullOrEmpty(fd.cFileName), "!String.IsNullOrEmpty(fd.cFileName)");
512 #endif
513
514             // build the long and short versions of the relative path
515             relativePathLong = fd.cFileName + Path.DirectorySeparatorChar + relativePathLong;
516             if (!String.IsNullOrEmpty(fd.cAlternateFileName)) {
517                 relativePathShort = fd.cAlternateFileName + Path.DirectorySeparatorChar + relativePathShort;
518             }
519             else {
520                 relativePathShort = fd.cFileName + Path.DirectorySeparatorChar + relativePathShort;
521             }
522
523             currentParentDir = Path.GetDirectoryName(currentParentDir);
524         }
525
526         if (!String.IsNullOrEmpty(relativePathLong)) {
527             data.PrependRelativePath(relativePathLong, relativePathShort);
528         }
529
530 #if DBG
531         Debug.Trace("FindFile", "fullPath=" + fullPath + ", rootDirectoryPath=" + rootDirectoryPath);
532         Debug.Trace("FindFile", "relativePathLong=" + relativePathLong + ", relativePathShort=" + relativePathShort);
533         string fileNameShort = data.FileNameShort == null ? "<null>" : data.FileNameShort;
534         Debug.Trace("FindFile", "FileNameLong=" + data.FileNameLong + ", FileNameShrot=" + fileNameShort);
535 #endif
536         
537         return hr;
538     }
539
540     internal FindFileData(ref UnsafeNativeMethods.WIN32_FIND_DATA wfd) {
541         _fileAttributesData = new FileAttributesData(ref wfd);
542         _fileNameLong = wfd.cFileName;
543         if (wfd.cAlternateFileName != null
544             && wfd.cAlternateFileName.Length > 0
545             && !StringUtil.EqualsIgnoreCase(wfd.cFileName, wfd.cAlternateFileName)) {
546             _fileNameShort = wfd.cAlternateFileName;
547         }
548     }
549     
550     private void PrependRelativePath(string relativePathLong, string relativePathShort) {
551         _fileNameLong = relativePathLong + _fileNameLong;
552
553         // if the short form is null or empty, prepend the short relative path to the long form
554         string fileName = String.IsNullOrEmpty(_fileNameShort) ? _fileNameLong : _fileNameShort;
555         _fileNameShort = relativePathShort + fileName;
556
557         // if the short form is the same as the long form, set the short form to null
558         if (StringUtil.EqualsIgnoreCase(_fileNameShort, _fileNameLong)) {
559             _fileNameShort = null;
560         }
561     }
562 }
563
564 //
565 // Wraps the Win32 API GetFileAttributesEx
566 // We use this api in addition to FindFirstFile because FindFirstFile
567 // does not work for volumes (e.g. "c:\")
568 //
569 sealed class FileAttributesData {
570     internal readonly FileAttributes    FileAttributes;
571     internal readonly DateTime          UtcCreationTime;
572     internal readonly DateTime          UtcLastAccessTime;
573     internal readonly DateTime          UtcLastWriteTime;
574     internal readonly long              FileSize;
575
576     static internal FileAttributesData NonExistantAttributesData {
577         get {
578             return new FileAttributesData();
579         }
580     }
581
582     static internal int GetFileAttributes(string path, out FileAttributesData fad) {
583         fad = null;
584
585         UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA  data;
586         if (!UnsafeNativeMethods.GetFileAttributesEx(path, UnsafeNativeMethods.GetFileExInfoStandard, out data)) {
587             return HttpException.HResultFromLastError(Marshal.GetLastWin32Error());
588         }
589
590         fad = new FileAttributesData(ref data);
591         return HResults.S_OK;
592     }
593
594     FileAttributesData() {
595         FileSize = -1;
596     }
597
598     FileAttributesData(ref UnsafeNativeMethods.WIN32_FILE_ATTRIBUTE_DATA data) {
599         FileAttributes    = (FileAttributes) data.fileAttributes;
600         UtcCreationTime   = DateTimeUtil.FromFileTimeToUtc(((long)data.ftCreationTimeHigh)   << 32 | (long)data.ftCreationTimeLow);
601         UtcLastAccessTime = DateTimeUtil.FromFileTimeToUtc(((long)data.ftLastAccessTimeHigh) << 32 | (long)data.ftLastAccessTimeLow);
602         UtcLastWriteTime  = DateTimeUtil.FromFileTimeToUtc(((long)data.ftLastWriteTimeHigh)  << 32 | (long)data.ftLastWriteTimeLow);
603         FileSize          = (long)(uint)data.fileSizeHigh << 32 | (long)(uint)data.fileSizeLow;
604     }
605
606     internal FileAttributesData(ref UnsafeNativeMethods.WIN32_FIND_DATA wfd) {
607         FileAttributes    = (FileAttributes) wfd.dwFileAttributes;
608         UtcCreationTime   = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftCreationTime_dwHighDateTime)   << 32 | (long)wfd.ftCreationTime_dwLowDateTime);
609         UtcLastAccessTime = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftLastAccessTime_dwHighDateTime) << 32 | (long)wfd.ftLastAccessTime_dwLowDateTime);
610         UtcLastWriteTime  = DateTimeUtil.FromFileTimeToUtc(((long)wfd.ftLastWriteTime_dwHighDateTime)  << 32 | (long)wfd.ftLastWriteTime_dwLowDateTime);
611         FileSize          = (long)wfd.nFileSizeHigh << 32 | (long)wfd.nFileSizeLow;
612     }
613
614 #if DBG
615     internal string DebugDescription(string indent) {
616         StringBuilder   sb = new StringBuilder(200);
617         string          i2 = indent + "    ";
618
619         sb.Append(indent + "FileAttributesData\n");
620         sb.Append(i2 + "FileAttributes: " + FileAttributes + "\n");
621         sb.Append(i2 + "  CreationTime: " + Debug.FormatUtcDate(UtcCreationTime) + "\n");
622         sb.Append(i2 + "LastAccessTime: " + Debug.FormatUtcDate(UtcLastAccessTime) + "\n");
623         sb.Append(i2 + " LastWriteTime: " + Debug.FormatUtcDate(UtcLastWriteTime) + "\n");
624         sb.Append(i2 + "      FileSize: " + FileSize.ToString("n0", NumberFormatInfo.InvariantInfo) + "\n");
625
626         return sb.ToString();
627     }
628 #endif
629
630 }
631
632 }