-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathFileUtilities.cs
1436 lines (1241 loc) · 55.1 KB
/
FileUtilities.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
#if !CLR2COMPATIBILITY
using System.Collections.Concurrent;
#else
using Microsoft.Build.Shared.Concurrent;
#endif
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Microsoft.Build.Utilities;
using Microsoft.Build.Shared.FileSystem;
namespace Microsoft.Build.Shared
{
/// <summary>
/// This class contains utility methods for file IO.
/// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in
/// each class get pulled into the resulting assembly.
/// </summary>
internal static partial class FileUtilities
{
// A list of possible test runners. If the program running has one of these substrings in the name, we assume
// this is a test harness.
// This flag, when set, indicates that we are running tests. Initially assume it's true. It also implies that
// the currentExecutableOverride is set to a path (that is non-null). Assume this is not initialized when we
// have the impossible combination of runningTests = false and currentExecutableOverride = null.
// This is the fake current executable we use in case we are running tests.
/// <summary>
/// The directory where MSBuild stores cache information used during the build.
/// </summary>
internal static string cacheDirectory = null;
/// <summary>
/// FOR UNIT TESTS ONLY
/// Clear out the static variable used for the cache directory so that tests that
/// modify it can validate their modifications.
/// </summary>
internal static void ClearCacheDirectoryPath()
{
cacheDirectory = null;
}
internal static readonly StringComparison PathComparison = GetIsFileSystemCaseSensitive() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
/// <summary>
/// Determines whether the file system is case sensitive.
/// Copied from https://github.com/dotnet/runtime/blob/73ba11f3015216b39cb866d9fb7d3d25e93489f2/src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs#L41-L59
/// </summary>
public static bool GetIsFileSystemCaseSensitive()
{
try
{
string pathWithUpperCase = Path.Combine(Path.GetTempPath(), "CASESENSITIVETEST" + Guid.NewGuid().ToString("N"));
using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
{
string lowerCased = pathWithUpperCase.ToLowerInvariant();
return !File.Exists(lowerCased);
}
}
catch (Exception exc)
{
// In case something goes terribly wrong, we don't want to fail just because
// of a casing test, so we assume case-insensitive-but-preserving.
Debug.Fail("Casing test failed: " + exc);
return false;
}
}
/// <summary>
/// Copied from https://github.com/dotnet/corefx/blob/056715ff70e14712419d82d51c8c50c54b9ea795/src/Common/src/System/IO/PathInternal.Windows.cs#L61
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
/// </summary>
internal static readonly char[] InvalidPathChars = new char[]
{
'|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31
};
/// <summary>
/// Copied from https://github.com/dotnet/corefx/blob/387cf98c410bdca8fd195b28cbe53af578698f94/src/System.Runtime.Extensions/src/System/IO/Path.Windows.cs#L18
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
/// </summary>
internal static readonly char[] InvalidFileNameChars = new char[]
{
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/'
};
internal static readonly char[] Slashes = { '/', '\\' };
internal static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString();
private static readonly ConcurrentDictionary<string, bool> FileExistenceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
private static readonly IFileSystem DefaultFileSystem = FileSystems.Default;
/// <summary>
/// Retrieves the MSBuild runtime cache directory
/// </summary>
internal static string GetCacheDirectory()
{
if (cacheDirectory == null)
{
cacheDirectory = Path.Combine(Path.GetTempPath(), String.Format(CultureInfo.CurrentUICulture, "MSBuild{0}-{1}", Process.GetCurrentProcess().Id, AppDomain.CurrentDomain.Id));
}
return cacheDirectory;
}
/// <summary>
/// Get the hex hash string for the string
/// </summary>
internal static string GetHexHash(string stringToHash)
{
return stringToHash.GetHashCode().ToString("X", CultureInfo.InvariantCulture);
}
/// <summary>
/// Get the hash for the assemblyPaths
/// </summary>
internal static int GetPathsHash(IEnumerable<string> assemblyPaths)
{
StringBuilder builder = new StringBuilder();
foreach (string path in assemblyPaths)
{
if (path != null)
{
string directoryPath = path.Trim();
if (directoryPath.Length > 0)
{
DateTime lastModifiedTime;
if (NativeMethodsShared.GetLastWriteDirectoryUtcTime(directoryPath, out lastModifiedTime))
{
builder.Append(lastModifiedTime.Ticks);
builder.Append('|');
builder.Append(directoryPath.ToUpperInvariant());
builder.Append('|');
}
}
}
}
return builder.ToString().GetHashCode();
}
/// <summary>
/// Clears the MSBuild runtime cache
/// </summary>
internal static void ClearCacheDirectory()
{
string cacheDirectory = GetCacheDirectory();
if (DefaultFileSystem.DirectoryExists(cacheDirectory))
{
DeleteDirectoryNoThrow(cacheDirectory, true);
}
}
/// <summary>
/// If the given path doesn't have a trailing slash then add one.
/// If the path is an empty string, does not modify it.
/// </summary>
/// <param name="fileSpec">The path to check.</param>
/// <returns>A path with a slash.</returns>
internal static string EnsureTrailingSlash(string fileSpec)
{
fileSpec = FixFilePath(fileSpec);
if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1]))
{
fileSpec += Path.DirectorySeparatorChar;
}
return fileSpec;
}
/// <summary>
/// Ensures the path does not have a leading or trailing slash after removing the first 'start' characters.
/// </summary>
internal static string EnsureNoLeadingOrTrailingSlash(string path, int start)
{
int stop = path.Length;
while (start < stop && IsSlash(path[start]))
{
start++;
}
while (start < stop && IsSlash(path[stop - 1]))
{
stop--;
}
return FixFilePath(path.Substring(start, stop - start));
}
/// <summary>
/// Ensures the path does not have a leading slash after removing the first 'start' characters but does end in a slash.
/// </summary>
internal static string EnsureTrailingNoLeadingSlash(string path, int start)
{
int stop = path.Length;
while (start < stop && IsSlash(path[start]))
{
start++;
}
return FixFilePath(start < stop && IsSlash(path[stop - 1]) ?
path.Substring(start) :
path.Substring(start) + Path.DirectorySeparatorChar);
}
/// <summary>
/// Ensures the path does not have a trailing slash.
/// </summary>
internal static string EnsureNoTrailingSlash(string path)
{
path = FixFilePath(path);
if (EndsWithSlash(path))
{
path = path.Substring(0, path.Length - 1);
}
return path;
}
/// <summary>
/// Indicates if the given file-spec ends with a slash.
/// </summary>
/// <param name="fileSpec">The file spec.</param>
/// <returns>true, if file-spec has trailing slash</returns>
internal static bool EndsWithSlash(string fileSpec)
{
return (fileSpec.Length > 0)
? IsSlash(fileSpec[fileSpec.Length - 1])
: false;
}
/// <summary>
/// Indicates if the given character is a slash.
/// </summary>
/// <param name="c"></param>
/// <returns>true, if slash</returns>
internal static bool IsSlash(char c)
{
return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar);
}
/// <summary>
/// Trims the string and removes any double quotes around it.
/// </summary>
internal static string TrimAndStripAnyQuotes(string path)
{
// Trim returns the same string if trimming isn't needed
path = path.Trim();
path = path.Trim(new char[] { '"' });
return path;
}
/// <summary>
/// Get the directory name of a rooted full path
/// </summary>
/// <param name="fullPath"></param>
/// <returns></returns>
internal static String GetDirectoryNameOfFullPath(String fullPath)
{
if (fullPath != null)
{
int i = fullPath.Length;
while (i > 0 && fullPath[--i] != Path.DirectorySeparatorChar && fullPath[i] != Path.AltDirectorySeparatorChar) ;
return FixFilePath(fullPath.Substring(0, i));
}
return null;
}
internal static string TruncatePathToTrailingSegments(string path, int trailingSegmentsToKeep)
{
#if !CLR2COMPATIBILITY
ErrorUtilities.VerifyThrowInternalLength(path, nameof(path));
ErrorUtilities.VerifyThrow(trailingSegmentsToKeep >= 0, "trailing segments must be positive");
var segments = path.Split(Slashes, StringSplitOptions.RemoveEmptyEntries);
var headingSegmentsToRemove = Math.Max(0, segments.Length - trailingSegmentsToKeep);
return string.Join(DirectorySeparatorString, segments.Skip(headingSegmentsToRemove));
#else
return path;
#endif
}
internal static bool ContainsRelativePathSegments(string path)
{
for (int i = 0; i < path.Length; i++)
{
if (i + 1 < path.Length && path[i] == '.' && path[i + 1] == '.')
{
if (RelativePathBoundsAreValid(path, i, i + 1))
{
return true;
}
else
{
i += 2;
continue;
}
}
if (path[i] == '.' && RelativePathBoundsAreValid(path, i, i))
{
return true;
}
}
return false;
}
#if !CLR2COMPATIBILITY
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static bool RelativePathBoundsAreValid(string path, int leftIndex, int rightIndex)
{
var leftBound = leftIndex - 1 >= 0
? path[leftIndex - 1]
: (char?)null;
var rightBound = rightIndex + 1 < path.Length
? path[rightIndex + 1]
: (char?)null;
return IsValidRelativePathBound(leftBound) && IsValidRelativePathBound(rightBound);
}
#if !CLR2COMPATIBILITY
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static bool IsValidRelativePathBound(char? c)
{
return c == null || IsAnySlash(c.Value);
}
/// <summary>
/// Gets the canonicalized full path of the provided path.
/// Guidance for use: call this on all paths accepted through public entry
/// points that need normalization. After that point, only verify the path
/// is rooted, using ErrorUtilities.VerifyThrowPathRooted.
/// ASSUMES INPUT IS ALREADY UNESCAPED.
/// </summary>
internal static string NormalizePath(string path)
{
ErrorUtilities.VerifyThrowArgumentLength(path, nameof(path));
string fullPath = GetFullPath(path);
return FixFilePath(fullPath);
}
internal static string NormalizePath(string directory, string file)
{
return NormalizePath(Path.Combine(directory, file));
}
#if !CLR2COMPATIBILITY
internal static string NormalizePath(params string[] paths)
{
return NormalizePath(Path.Combine(paths));
}
#endif
private static string GetFullPath(string path)
{
#if FEATURE_LEGACY_GETFULLPATH
if (NativeMethodsShared.IsWindows)
{
string uncheckedFullPath = NativeMethodsShared.GetFullPath(path);
if (IsPathTooLong(uncheckedFullPath))
{
string message = ResourceUtilities.FormatString(AssemblyResources.GetString("Shared.PathTooLong"), path, NativeMethodsShared.MaxPath);
throw new PathTooLongException(message);
}
// We really don't care about extensions here, but Path.HasExtension provides a great way to
// invoke the CLR's invalid path checks (these are independent of path length)
Path.HasExtension(uncheckedFullPath);
// If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting
// and security checks for strings like \\?\GlobalRoot
return IsUNCPath(uncheckedFullPath) ? Path.GetFullPath(uncheckedFullPath) : uncheckedFullPath;
}
#endif
return Path.GetFullPath(path);
}
#if FEATURE_LEGACY_GETFULLPATH
private static bool IsUNCPath(string path)
{
if (!NativeMethodsShared.IsWindows || !path.StartsWith(@"\\", StringComparison.Ordinal))
{
return false;
}
bool isUNC = true;
for (int i = 2; i < path.Length - 1; i++)
{
if (path[i] == '\\')
{
isUNC = false;
break;
}
}
/*
From Path.cs in the CLR
Throw an ArgumentException for paths like \\, \\server, \\server\
This check can only be properly done after normalizing, so
\\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\
(an internal kernel path) because it provides aliases for drives.
throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
// Check for \\?\Globalroot, an internal mechanism to the kernel
// that provides aliases for drives and other undocumented stuff.
// The kernel team won't even describe the full set of what
// is available here - we don't want managed apps mucking
// with this for security reasons.
*/
return isUNC || path.IndexOf(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase) != -1;
}
#endif // FEATURE_LEGACY_GETFULLPATH
internal static string FixFilePath(string path)
{
return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/');//.Replace("//", "/");
}
#if !CLR2COMPATIBILITY
/// <summary>
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
/// The heuristic is if something resembles paths (contains slashes) check if the
/// first segment exists and is a directory.
/// Use a native shared method to massage file path. If the file is adjusted,
/// that qualifies is as a path.
///
/// @baseDirectory is just passed to LooksLikeUnixFilePath, to help with the check
/// </summary>
internal static string MaybeAdjustFilePath(string value, string baseDirectory = "")
{
var comparisonType = StringComparison.Ordinal;
// Don't bother with arrays or properties or network paths, or those that
// have no slashes.
if (NativeMethodsShared.IsWindows || string.IsNullOrEmpty(value)
|| value.StartsWith("$(", comparisonType) || value.StartsWith("@(", comparisonType)
|| value.StartsWith("\\\\", comparisonType))
{
return value;
}
// For Unix-like systems, we may want to convert backslashes to slashes
Span<char> newValue = ConvertToUnixSlashes(value.ToCharArray());
// Find the part of the name we want to check, that is remove quotes, if present
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
return shouldAdjust ? newValue.ToString() : value;
}
/// <summary>
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
/// This overload takes and returns ReadOnlyMemory of characters.
/// </summary>
internal static ReadOnlyMemory<char> MaybeAdjustFilePath(ReadOnlyMemory<char> value, string baseDirectory = "")
{
if (NativeMethodsShared.IsWindows || value.IsEmpty)
{
return value;
}
// Don't bother with arrays or properties or network paths.
if (value.Length >= 2)
{
var span = value.Span;
// The condition is equivalent to span.StartsWith("$(") || span.StartsWith("@(") || span.StartsWith("\\\\")
if ((span[1] == '(' && (span[0] == '$' || span[0] == '@')) ||
(span[1] == '\\' && span[0] == '\\'))
{
return value;
}
}
// For Unix-like systems, we may want to convert backslashes to slashes
Span<char> newValue = ConvertToUnixSlashes(value.ToArray());
// Find the part of the name we want to check, that is remove quotes, if present
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
return shouldAdjust ? newValue.ToString().AsMemory() : value;
}
private static Span<char> ConvertToUnixSlashes(Span<char> path)
{
return path.IndexOf('\\') == -1 ? path : CollapseSlashes(path);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Span<char> CollapseSlashes(Span<char> str)
{
int sliceLength = 0;
// Performs Regex.Replace(str, @"[\\/]+", "/")
for (int i = 0; i < str.Length; i++)
{
bool isCurSlash = IsAnySlash(str[i]);
bool isPrevSlash = i > 0 && IsAnySlash(str[i - 1]);
if (!isCurSlash || !isPrevSlash)
{
str[sliceLength] = str[i] == '\\' ? '/' : str[i];
sliceLength++;
}
}
return str.Slice(0, sliceLength);
}
private static Span<char> RemoveQuotes(Span<char> path)
{
int endId = path.Length - 1;
char singleQuote = '\'';
char doubleQuote = '\"';
bool hasQuotes = path.Length > 2
&& ((path[0] == singleQuote && path[endId] == singleQuote)
|| (path[0] == doubleQuote && path[endId] == doubleQuote));
return hasQuotes ? path.Slice(1, endId - 1) : path;
}
#endif
#if !CLR2COMPATIBILITY
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
internal static bool IsAnySlash(char c) => c == '/' || c == '\\';
#if !CLR2COMPATIBILITY
/// <summary>
/// If on Unix, check if the string looks like a file path.
/// The heuristic is if something resembles paths (contains slashes) check if the
/// first segment exists and is a directory.
///
/// If @baseDirectory is not null, then look for the first segment exists under
/// that
/// </summary>
internal static bool LooksLikeUnixFilePath(string value, string baseDirectory = "")
=> LooksLikeUnixFilePath(value.AsSpan(), baseDirectory);
internal static bool LooksLikeUnixFilePath(ReadOnlySpan<char> value, string baseDirectory = "")
{
if (NativeMethodsShared.IsWindows)
{
return false;
}
// The first slash will either be at the beginning of the string or after the first directory name
int directoryLength = value.Slice(1).IndexOf('/') + 1;
bool shouldCheckDirectory = directoryLength != 0;
// Check for actual files or directories under / that get missed by the above logic
bool shouldCheckFileOrDirectory = !shouldCheckDirectory && value.Length > 0 && value[0] == '/';
ReadOnlySpan<char> directory = value.Slice(0, directoryLength);
return (shouldCheckDirectory && DefaultFileSystem.DirectoryExists(Path.Combine(baseDirectory, directory.ToString())))
|| (shouldCheckFileOrDirectory && DefaultFileSystem.DirectoryEntryExists(value.ToString()));
}
#endif
/// <summary>
/// Extracts the directory from the given file-spec.
/// </summary>
/// <param name="fileSpec">The filespec.</param>
/// <returns>directory path</returns>
internal static string GetDirectory(string fileSpec)
{
string directory = Path.GetDirectoryName(FixFilePath(fileSpec));
// if file-spec is a root directory e.g. c:, c:\, \, \\server\share
// NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server
if (directory == null)
{
// just use the file-spec as-is
directory = fileSpec;
}
else if ((directory.Length > 0) && !EndsWithSlash(directory))
{
// restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories)
directory += Path.DirectorySeparatorChar;
}
return directory;
}
/// <summary>
/// Determines whether the given assembly file name has one of the listed extensions.
/// </summary>
/// <param name="fileName">The name of the file</param>
/// <param name="allowedExtensions">Array of extensions to consider.</param>
/// <returns></returns>
internal static bool HasExtension(string fileName, string[] allowedExtensions)
{
Debug.Assert(allowedExtensions?.Length > 0);
// Easiest way to invoke invalid path chars
// check, which callers are relying on.
if (Path.HasExtension(fileName))
{
foreach (string extension in allowedExtensions)
{
Debug.Assert(!String.IsNullOrEmpty(extension) && extension[0] == '.');
if (fileName.EndsWith(extension, PathComparison))
{
return true;
}
}
}
return false;
}
// ISO 8601 Universal time with sortable format
internal const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff";
/// <summary>
/// Get the currently executing assembly path
/// </summary>
internal static string ExecutingAssemblyPath => Path.GetFullPath(AssemblyUtilities.GetAssemblyLocation(typeof(FileUtilities).GetTypeInfo().Assembly));
/// <summary>
/// Determines the full path for the given file-spec.
/// ASSUMES INPUT IS STILL ESCAPED
/// </summary>
/// <param name="fileSpec">The file spec to get the full path of.</param>
/// <param name="currentDirectory"></param>
/// <returns>full path</returns>
internal static string GetFullPath(string fileSpec, string currentDirectory)
{
// Sending data out of the engine into the filesystem, so time to unescape.
fileSpec = FixFilePath(EscapingUtilities.UnescapeAll(fileSpec));
// Data coming back from the filesystem into the engine, so time to escape it back.
string fullPath = EscapingUtilities.Escape(NormalizePath(Path.Combine(currentDirectory, fileSpec)));
if (NativeMethodsShared.IsWindows && !EndsWithSlash(fullPath))
{
if (FileUtilitiesRegex.IsDrivePattern(fileSpec) ||
FileUtilitiesRegex.IsUncPattern(fullPath))
{
// append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares)
fullPath += Path.DirectorySeparatorChar;
}
}
return fullPath;
}
/// <summary>
/// A variation of Path.GetFullPath that will return the input value
/// instead of throwing any IO exception.
/// Useful to get a better path for an error message, without the risk of throwing
/// if the error message was itself caused by the path being invalid!
/// </summary>
internal static string GetFullPathNoThrow(string path)
{
try
{
path = NormalizePath(path);
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
return path;
}
/// <summary>
/// Compare if two paths, relative to the given currentDirectory are equal.
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <param name="currentDirectory"></param>
/// <param name="alwaysIgnoreCase"></param>
/// <returns></returns>
internal static bool ComparePathsNoThrow(string first, string second, string currentDirectory, bool alwaysIgnoreCase = false)
{
StringComparison pathComparison = alwaysIgnoreCase ? StringComparison.OrdinalIgnoreCase : PathComparison;
// perf: try comparing the bare strings first
if (string.Equals(first, second, pathComparison))
{
return true;
}
var firstFullPath = NormalizePathForComparisonNoThrow(first, currentDirectory);
var secondFullPath = NormalizePathForComparisonNoThrow(second, currentDirectory);
return string.Equals(firstFullPath, secondFullPath, pathComparison);
}
/// <summary>
/// Normalizes a path for path comparison
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
///
/// </summary>
internal static string NormalizePathForComparisonNoThrow(string path, string currentDirectory)
{
// file is invalid, return early to avoid triggering an exception
if (PathIsInvalid(path))
{
return path;
}
var normalizedPath = path.NormalizeForPathComparison();
var fullPath = GetFullPathNoThrow(Path.Combine(currentDirectory, normalizedPath));
return fullPath;
}
internal static bool PathIsInvalid(string path)
{
if (path.IndexOfAny(InvalidPathChars) >= 0)
{
return true;
}
// Path.GetFileName does not react well to malformed filenames.
// For example, Path.GetFileName("a/b/foo:bar") returns bar instead of foo:bar
// It also throws exceptions on illegal path characters
var lastDirectorySeparator = path.LastIndexOfAny(Slashes);
return path.IndexOfAny(InvalidFileNameChars, lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0) >= 0;
}
/// <summary>
/// A variation on File.Delete that will throw ExceptionHandling.NotExpectedException exceptions
/// </summary>
internal static void DeleteNoThrow(string path)
{
try
{
File.Delete(FixFilePath(path));
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
}
/// <summary>
/// A variation on Directory.Delete that will throw ExceptionHandling.NotExpectedException exceptions
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Int32.TryParse(System.String,System.Int32@)", Justification = "We expect the out value to be 0 if the parse fails and compensate accordingly")]
internal static void DeleteDirectoryNoThrow(string path, bool recursive, int retryCount = 0, int retryTimeOut = 0)
{
// Try parse will set the out parameter to 0 if the string passed in is null, or is outside the range of an int.
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETERETRYCOUNT"), out retryCount))
{
retryCount = 0;
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETRETRYTIMEOUT"), out retryTimeOut))
{
retryTimeOut = 0;
}
retryCount = retryCount < 1 ? 2 : retryCount;
retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut;
path = FixFilePath(path);
for (int i = 0; i < retryCount; i++)
{
try
{
if (DefaultFileSystem.DirectoryExists(path))
{
Directory.Delete(path, recursive);
break;
}
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
if (i + 1 < retryCount) // should not wait for the final iteration since we not gonna check anyway
{
Thread.Sleep(retryTimeOut);
}
}
}
/// <summary>
/// Deletes a directory, ensuring that Directory.Delete does not get a path ending in a slash.
/// </summary>
/// <remarks>
/// This is a workaround for https://github.com/dotnet/corefx/issues/3780, which clashed with a common
/// pattern in our tests.
/// </remarks>
internal static void DeleteWithoutTrailingBackslash(string path, bool recursive = false)
{
// Some tests (such as FileMatcher and Evaluation tests) were failing with an UnauthorizedAccessException or directory not empty.
// This retry logic works around that issue.
const int NUM_TRIES = 3;
for (int i = 0; i < NUM_TRIES; i++)
{
try
{
Directory.Delete(EnsureNoTrailingSlash(path), recursive);
// If we got here, the directory was successfully deleted
return;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
if (i == NUM_TRIES - 1)
{
//var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
//string fileString = string.Join(Environment.NewLine, files);
//string message = $"Unable to delete directory '{path}'. Contents:" + Environment.NewLine + fileString;
//throw new IOException(message, ex);
throw;
}
}
Thread.Sleep(10);
}
}
/// <summary>
/// Gets a file info object for the specified file path. If the file path
/// is invalid, or is a directory, or cannot be accessed, or does not exist,
/// it returns null rather than throwing or returning a FileInfo around a non-existent file.
/// This allows it to be called where File.Exists() (which never throws, and returns false
/// for directories) was called - but with the advantage that a FileInfo object is returned
/// that can be queried (e.g., for LastWriteTime) without hitting the disk again.
/// </summary>
/// <param name="filePath"></param>
/// <returns>FileInfo around path if it is an existing /file/, else null</returns>
internal static FileInfo GetFileInfoNoThrow(string filePath)
{
filePath = AttemptToShortenPath(filePath);
FileInfo fileInfo;
try
{
fileInfo = new FileInfo(filePath);
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
// Invalid or inaccessible path: treat as if nonexistent file, just as File.Exists does
return null;
}
if (fileInfo.Exists)
{
// It's an existing file
return fileInfo;
}
else
{
// Nonexistent, or existing but a directory, just as File.Exists behaves
return null;
}
}
/// <summary>
/// Returns if the directory exists
/// </summary>
/// <param name="fullPath">Full path to the directory in the filesystem</param>
/// <param name="fileSystem">The file system</param>
/// <returns></returns>
internal static bool DirectoryExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.DirectoryExists)
: fileSystem.DirectoryExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// Returns if the directory exists
/// </summary>
/// <param name="fullPath">Full path to the file in the filesystem</param>
/// <param name="fileSystem">The file system</param>
/// <returns></returns>
internal static bool FileExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.FileExists)
: fileSystem.FileExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// If there is a directory or file at the specified path, returns true.
/// Otherwise, returns false.
/// Does not throw IO exceptions, to match Directory.Exists and File.Exists.
/// Unlike calling each of those in turn it only accesses the disk once, which is faster.
/// </summary>
internal static bool FileOrDirectoryExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.DirectoryEntryExists)
: fileSystem.DirectoryEntryExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// This method returns true if the specified filename is a solution file (.sln) or
/// solution filter file (.slnf); otherwise, it returns false.
/// </summary>
/// <remarks>
/// Solution filters are included because they are a thin veneer over solutions, just
/// with a more limited set of projects to build, and should be treated the same way.
/// </remarks>
internal static bool IsSolutionFilename(string filename)
{
return HasExtension(filename, ".sln") || HasExtension(filename, ".slnf");
}
internal static bool IsSolutionFilterFilename(string filename)
{
return HasExtension(filename, ".slnf");
}
/// <summary>
/// Returns true if the specified filename is a VC++ project file, otherwise returns false
/// </summary>
internal static bool IsVCProjFilename(string filename)
{
return HasExtension(filename, ".vcproj");
}
internal static bool IsDspFilename(string filename)
{
return HasExtension(filename, ".dsp");
}
/// <summary>
/// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false.
/// </summary>
internal static bool IsMetaprojectFilename(string filename)
{
return HasExtension(filename, ".metaproj");
}