Skip to content

Commit f7ba49d

Browse files
authored
Remove validation to stat call for symlinks since is a breaking change (#57551)
* Remove validation to stat call for symlinks since is a breaking change, subsequently remove the symlink cache logic as is no longer needed * Undo try-catch workaround in PhysicalFileProvider * Fix tests that were failing due to changes
1 parent 52ae8c3 commit f7ba49d

File tree

5 files changed

+92
-110
lines changed

5 files changed

+92
-110
lines changed

src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,7 @@ private DateTime GetLastWriteTimeUtc()
5454
return DateTime.MinValue;
5555
}
5656

57-
DateTime? lastWriteTimeUtc = FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo);
58-
59-
if (lastWriteTimeUtc == null)
60-
{
61-
try
62-
{
63-
lastWriteTimeUtc = _fileInfo.LastWriteTimeUtc;
64-
}
65-
catch (IOException) { } // https://github.com/dotnet/runtime/issues/57221
66-
}
67-
68-
return lastWriteTimeUtc ?? DateTime.MinValue;
57+
return FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo) ?? _fileInfo.LastWriteTimeUtc;
6958
}
7059

7160
/// <summary>

src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink()
2121
return testDirectory;
2222
}
2323

24+
protected DirectoryInfo CreateSelfReferencingSymbolicLink()
25+
{
26+
string path = GetRandomDirPath();
27+
return (DirectoryInfo)Directory.CreateSymbolicLink(path, path);
28+
}
29+
2430
protected string GetRandomFileName() => GetTestFileName() + ".txt";
2531
protected string GetRandomLinkName() => GetTestFileName() + ".link";
2632
protected string GetRandomDirName() => GetTestFileName() + "_dir";

src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -52,72 +52,6 @@ protected override void AssertLinkExists(FileSystemInfo link)
5252
}
5353
}
5454

55-
[Theory]
56-
[InlineData(false)]
57-
[InlineData(true)]
58-
public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
59-
{
60-
var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
61-
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
62-
63-
// Windows avoids accessing the cyclic symlink if we do not recurse
64-
if (OperatingSystem.IsWindows() && !recurse)
65-
{
66-
testDirectory.EnumerateDirectories("*", options).Count();
67-
testDirectory.GetDirectories("*", options).Count();
68-
}
69-
else
70-
{
71-
// Internally transforms the FileSystemEntry to a DirectoryInfo, which performs a disk hit on the cyclic symlink
72-
Assert.Throws<IOException>(() => testDirectory.EnumerateDirectories("*", options).Count());
73-
Assert.Throws<IOException>(() => testDirectory.GetDirectories("*", options).Count());
74-
}
75-
}
76-
77-
[Theory]
78-
[InlineData(false)]
79-
[InlineData(true)]
80-
public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
81-
{
82-
var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
83-
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
84-
85-
// Windows avoids accessing the cyclic symlink if we do not recurse
86-
if (OperatingSystem.IsWindows() && !recurse)
87-
{
88-
testDirectory.EnumerateFiles("*", options).Count();
89-
testDirectory.GetFiles("*", options).Count();
90-
}
91-
else
92-
{
93-
// Internally transforms the FileSystemEntry to a FileInfo, which performs a disk hit on the cyclic symlink
94-
Assert.Throws<IOException>(() => testDirectory.EnumerateFiles("*", options).Count());
95-
Assert.Throws<IOException>(() => testDirectory.GetFiles("*", options).Count());
96-
}
97-
}
98-
99-
[Theory]
100-
[InlineData(false)]
101-
[InlineData(true)]
102-
public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
103-
{
104-
var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
105-
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
106-
107-
// Windows avoids accessing the cyclic symlink if we do not recurse
108-
if (OperatingSystem.IsWindows() && !recurse)
109-
{
110-
testDirectory.EnumerateFileSystemInfos("*", options).Count();
111-
testDirectory.GetFileSystemInfos("*", options).Count();
112-
}
113-
else
114-
{
115-
// Internally transforms the FileSystemEntry to a FileSystemInfo, which performs a disk hit on the cyclic symlink
116-
Assert.Throws<IOException>(() => testDirectory.EnumerateFileSystemInfos("*", options).Count());
117-
Assert.Throws<IOException>(() => testDirectory.GetFileSystemInfos("*", options).Count());
118-
}
119-
}
120-
12155
[Fact]
12256
public void ResolveLinkTarget_Throws_NotExists() =>
12357
ResolveLinkTarget_Throws_NotExists_Internal<DirectoryNotFoundException>();

src/libraries/System.IO.FileSystem/tests/Enumeration/SymbolicLinksTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,81 @@ public void EnumerateFileSystemEntries_LinksWithCycles_ShouldNotThrow()
6161

6262
Assert.Single(enumerable);
6363
}
64+
65+
[ConditionalTheory(nameof(CanCreateSymbolicLinks))]
66+
[InlineData(false, false)] // OK
67+
[InlineData(false, true)] // throw
68+
[InlineData(true, false)] // throw, OK on Unix
69+
[InlineData(true, true)] // throw
70+
public void EnumerateGet_SelfReferencingLink_Instance(bool recurse, bool linkAsRoot)
71+
{
72+
var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
73+
74+
DirectoryInfo testDirectory = linkAsRoot ?
75+
CreateSelfReferencingSymbolicLink() :
76+
CreateDirectoryContainingSelfReferencingSymbolicLink();
77+
78+
// Unix doesn't have a problem when it steps in a self-referencing link through the directory recursion.
79+
if ((!recurse || !OperatingSystem.IsWindows()) && !linkAsRoot)
80+
{
81+
testDirectory.EnumerateFileSystemInfos("*", options).Count();
82+
testDirectory.GetFileSystemInfos("*", options).Count();
83+
84+
testDirectory.EnumerateDirectories("*", options).Count();
85+
testDirectory.GetDirectories("*", options).Count();
86+
87+
testDirectory.EnumerateFiles("*", options).Count();
88+
testDirectory.GetFiles("*", options).Count();
89+
}
90+
else
91+
{
92+
Assert.Throws<IOException>(() => testDirectory.EnumerateFileSystemInfos("*", options).Count());
93+
Assert.Throws<IOException>(() => testDirectory.GetFileSystemInfos("*", options).Count());
94+
95+
Assert.Throws<IOException>(() => testDirectory.EnumerateDirectories("*", options).Count());
96+
Assert.Throws<IOException>(() => testDirectory.GetDirectories("*", options).Count());
97+
98+
Assert.Throws<IOException>(() => testDirectory.EnumerateFiles("*", options).Count());
99+
Assert.Throws<IOException>(() => testDirectory.GetFiles("*", options).Count());
100+
}
101+
}
102+
103+
[ConditionalTheory(nameof(CanCreateSymbolicLinks))]
104+
[InlineData(false, false)] // OK
105+
[InlineData(false, true)] // throw
106+
[InlineData(true, false)] // throw, OK on Unix
107+
[InlineData(true, true)] // throw
108+
public void EnumerateGet_SelfReferencingLink_Static(bool recurse, bool linkAsRoot)
109+
{
110+
var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
111+
112+
DirectoryInfo testDirectory = linkAsRoot ?
113+
CreateSelfReferencingSymbolicLink() :
114+
CreateDirectoryContainingSelfReferencingSymbolicLink();
115+
116+
// Unix doesn't have a problem when it steps in a self-referencing link through the directory recursion.
117+
if ((!recurse || !OperatingSystem.IsWindows()) && !linkAsRoot)
118+
{
119+
Directory.EnumerateFileSystemEntries(testDirectory.FullName, "*", options).Count();
120+
Directory.GetFileSystemEntries(testDirectory.FullName, "*", options).Count();
121+
122+
Directory.EnumerateDirectories(testDirectory.FullName, "*", options).Count();
123+
Directory.GetDirectories(testDirectory.FullName, "*", options).Count();
124+
125+
Directory.EnumerateFiles(testDirectory.FullName, "*", options).Count();
126+
Directory.GetFiles(testDirectory.FullName, "*", options).Count();
127+
}
128+
else
129+
{
130+
Assert.Throws<IOException>(() => Directory.EnumerateFileSystemEntries(testDirectory.FullName, "*", options).Count());
131+
Assert.Throws<IOException>(() => Directory.GetFileSystemEntries(testDirectory.FullName, "*", options).Count());
132+
133+
Assert.Throws<IOException>(() => Directory.EnumerateDirectories(testDirectory.FullName, "*", options).Count());
134+
Assert.Throws<IOException>(() => Directory.GetDirectories(testDirectory.FullName, "*", options).Count());
135+
136+
Assert.Throws<IOException>(() => Directory.EnumerateFiles(testDirectory.FullName, "*", options).Count());
137+
Assert.Throws<IOException>(() => Directory.GetFiles(testDirectory.FullName, "*", options).Count());
138+
}
139+
}
64140
}
65141
}

src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ internal struct FileStatus
1818
// Positive number: the error code returned by the lstat call
1919
private int _initializedFileCache;
2020

21-
// The last cached stat information about the file
22-
// Refresh only collects this if lstat determines the path is a symbolic link
23-
private Interop.Sys.FileStatus _symlinkCache;
24-
25-
// -1: if the symlink cache isn't initialized - Refresh only changes this value if lstat determines the path is a symbolic link
26-
// 0: if the symlink cache was initialized with no errors
27-
// Positive number: the error code returned by the stat call
28-
private int _initializedSymlinkCache;
29-
3021
// We track intent of creation to know whether or not we want to (1) create a
3122
// DirectoryInfo around this status struct or (2) actually are part of a DirectoryInfo.
3223
// Set to true during initialization when the DirectoryEntry's INodeType describes a directory
@@ -113,7 +104,6 @@ private bool HasSymbolicLinkFlag
113104
internal void InvalidateCaches()
114105
{
115106
_initializedFileCache = -1;
116-
_initializedSymlinkCache = -1;
117107
}
118108

119109
internal bool IsReadOnly(ReadOnlySpan<char> path, bool continueOnError = false)
@@ -340,17 +330,16 @@ internal long GetLength(ReadOnlySpan<char> path, bool continueOnError = false)
340330
return _fileCache.Size;
341331
}
342332

343-
// Tries to refresh the lstat cache (_fileCache) and, if the file is pointing to a symbolic link, then also the stat cache (_symlinkCache)
333+
// Tries to refresh the lstat cache (_fileCache).
344334
// This method should not throw. Instead, we store the results, and we will throw when the user attempts to access any of the properties when there was a failure
345335
internal void RefreshCaches(ReadOnlySpan<char> path)
346336
{
347337
_isDirectory = false;
348338
path = Path.TrimEndingDirectorySeparator(path);
349339

350340
// Retrieve the file cache (lstat) to get the details on the object, without following symlinks.
351-
// If it is a symlink, then subsequently get details on the target of the symlink,
352-
// storing those results separately. We only report failure if the initial
353-
// lstat fails, as a broken symlink should still report info on exists, attributes, etc.
341+
// If it is a symlink, then subsequently get details on the target of the symlink.
342+
// We only report failure if the initial lstat fails, as a broken symlink should still report info on exists, attributes, etc.
354343
if (!TryRefreshFileCache(path))
355344
{
356345
_exists = false;
@@ -361,11 +350,11 @@ internal void RefreshCaches(ReadOnlySpan<char> path)
361350
_isDirectory = CacheHasDirectoryFlag(_fileCache);
362351

363352
// We also need to check if the main path is a symbolic link,
364-
// in which case, we retrieve the symbolic link data
365-
if (!_isDirectory && HasSymbolicLinkFlag && TryRefreshSymbolicLinkCache(path))
353+
// in which case, we retrieve the symbolic link's target data
354+
if (!_isDirectory && HasSymbolicLinkFlag && Interop.Sys.Stat(path, out Interop.Sys.FileStatus target) == 0)
366355
{
367356
// and check again if the symlink path is a directory
368-
_isDirectory = CacheHasDirectoryFlag(_symlinkCache);
357+
_isDirectory = CacheHasDirectoryFlag(target);
369358
}
370359

371360
#if !TARGET_BROWSER
@@ -376,16 +365,15 @@ internal void RefreshCaches(ReadOnlySpan<char> path)
376365

377366
_exists = true;
378367

379-
// Checks if the specified cache (lstat=_fileCache, stat=_symlinkCache) has the directory attribute set
368+
// Checks if the specified cache has the directory attribute set
380369
// Only call if Refresh has been successfully called at least once, and you're
381370
// certain the passed-in cache was successfully retrieved
382371
static bool CacheHasDirectoryFlag(Interop.Sys.FileStatus cache) =>
383372
(cache.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
384373
}
385374

386375
// Checks if the file cache is set to -1 and refreshes it's value.
387-
// Optionally, if the symlink cache is set to -1 and the file cache determined the path is pointing to a symbolic link, this cache is also refreshed.
388-
// If stat or lstat failed, and continueOnError is set to true, this method will throw.
376+
// If it failed, and continueOnError is set to true, this method will throw.
389377
internal void EnsureCachesInitialized(ReadOnlySpan<char> path, bool continueOnError = false)
390378
{
391379
if (_initializedFileCache == -1)
@@ -402,18 +390,10 @@ internal void EnsureCachesInitialized(ReadOnlySpan<char> path, bool continueOnEr
402390
// Throws if any of the caches has an error number saved in it
403391
private void ThrowOnCacheInitializationError(ReadOnlySpan<char> path)
404392
{
405-
int errno;
406393
// Lstat should always be initialized by Refresh
407394
if (_initializedFileCache != 0)
408395
{
409-
errno = _initializedFileCache;
410-
InvalidateCaches();
411-
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path));
412-
}
413-
// Stat is optionally initialized when Refresh detects object is a symbolic link
414-
else if (_initializedSymlinkCache != 0 && _initializedSymlinkCache != -1)
415-
{
416-
errno = _initializedSymlinkCache;
396+
int errno = _initializedFileCache;
417397
InvalidateCaches();
418398
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path));
419399
}
@@ -422,9 +402,6 @@ private void ThrowOnCacheInitializationError(ReadOnlySpan<char> path)
422402
private bool TryRefreshFileCache(ReadOnlySpan<char> path) =>
423403
VerifyStatCall(Interop.Sys.LStat(path, out _fileCache), out _initializedFileCache);
424404

425-
private bool TryRefreshSymbolicLinkCache(ReadOnlySpan<char> path) =>
426-
VerifyStatCall(Interop.Sys.Stat(path, out _symlinkCache), out _initializedSymlinkCache);
427-
428405
// Receives the return value of a stat or lstat call.
429406
// If the call is unsuccessful, sets the initialized parameter to a positive number representing the last error info.
430407
// If the call is successful, sets the initialized parameter to zero.

0 commit comments

Comments
 (0)