* 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
return DateTime.MinValue;
}
- DateTime? lastWriteTimeUtc = FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo);
-
- if (lastWriteTimeUtc == null)
- {
- try
- {
- lastWriteTimeUtc = _fileInfo.LastWriteTimeUtc;
- }
- catch (IOException) { } // https://github.com/dotnet/runtime/issues/57221
- }
-
- return lastWriteTimeUtc ?? DateTime.MinValue;
+ return FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo) ?? _fileInfo.LastWriteTimeUtc;
}
/// <summary>
return testDirectory;
}
+ protected DirectoryInfo CreateSelfReferencingSymbolicLink()
+ {
+ string path = GetRandomDirPath();
+ return (DirectoryInfo)Directory.CreateSymbolicLink(path, path);
+ }
+
protected string GetRandomFileName() => GetTestFileName() + ".txt";
protected string GetRandomLinkName() => GetTestFileName() + ".link";
protected string GetRandomDirName() => GetTestFileName() + "_dir";
}
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
- {
- var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
- DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
-
- // Windows avoids accessing the cyclic symlink if we do not recurse
- if (OperatingSystem.IsWindows() && !recurse)
- {
- testDirectory.EnumerateDirectories("*", options).Count();
- testDirectory.GetDirectories("*", options).Count();
- }
- else
- {
- // Internally transforms the FileSystemEntry to a DirectoryInfo, which performs a disk hit on the cyclic symlink
- Assert.Throws<IOException>(() => testDirectory.EnumerateDirectories("*", options).Count());
- Assert.Throws<IOException>(() => testDirectory.GetDirectories("*", options).Count());
- }
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
- {
- var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
- DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
-
- // Windows avoids accessing the cyclic symlink if we do not recurse
- if (OperatingSystem.IsWindows() && !recurse)
- {
- testDirectory.EnumerateFiles("*", options).Count();
- testDirectory.GetFiles("*", options).Count();
- }
- else
- {
- // Internally transforms the FileSystemEntry to a FileInfo, which performs a disk hit on the cyclic symlink
- Assert.Throws<IOException>(() => testDirectory.EnumerateFiles("*", options).Count());
- Assert.Throws<IOException>(() => testDirectory.GetFiles("*", options).Count());
- }
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
- {
- var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
- DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
-
- // Windows avoids accessing the cyclic symlink if we do not recurse
- if (OperatingSystem.IsWindows() && !recurse)
- {
- testDirectory.EnumerateFileSystemInfos("*", options).Count();
- testDirectory.GetFileSystemInfos("*", options).Count();
- }
- else
- {
- // Internally transforms the FileSystemEntry to a FileSystemInfo, which performs a disk hit on the cyclic symlink
- Assert.Throws<IOException>(() => testDirectory.EnumerateFileSystemInfos("*", options).Count());
- Assert.Throws<IOException>(() => testDirectory.GetFileSystemInfos("*", options).Count());
- }
- }
-
[Fact]
public void ResolveLinkTarget_Throws_NotExists() =>
ResolveLinkTarget_Throws_NotExists_Internal<DirectoryNotFoundException>();
Assert.Single(enumerable);
}
+
+ [ConditionalTheory(nameof(CanCreateSymbolicLinks))]
+ [InlineData(false, false)] // OK
+ [InlineData(false, true)] // throw
+ [InlineData(true, false)] // throw, OK on Unix
+ [InlineData(true, true)] // throw
+ public void EnumerateGet_SelfReferencingLink_Instance(bool recurse, bool linkAsRoot)
+ {
+ var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
+
+ DirectoryInfo testDirectory = linkAsRoot ?
+ CreateSelfReferencingSymbolicLink() :
+ CreateDirectoryContainingSelfReferencingSymbolicLink();
+
+ // Unix doesn't have a problem when it steps in a self-referencing link through the directory recursion.
+ if ((!recurse || !OperatingSystem.IsWindows()) && !linkAsRoot)
+ {
+ testDirectory.EnumerateFileSystemInfos("*", options).Count();
+ testDirectory.GetFileSystemInfos("*", options).Count();
+
+ testDirectory.EnumerateDirectories("*", options).Count();
+ testDirectory.GetDirectories("*", options).Count();
+
+ testDirectory.EnumerateFiles("*", options).Count();
+ testDirectory.GetFiles("*", options).Count();
+ }
+ else
+ {
+ Assert.Throws<IOException>(() => testDirectory.EnumerateFileSystemInfos("*", options).Count());
+ Assert.Throws<IOException>(() => testDirectory.GetFileSystemInfos("*", options).Count());
+
+ Assert.Throws<IOException>(() => testDirectory.EnumerateDirectories("*", options).Count());
+ Assert.Throws<IOException>(() => testDirectory.GetDirectories("*", options).Count());
+
+ Assert.Throws<IOException>(() => testDirectory.EnumerateFiles("*", options).Count());
+ Assert.Throws<IOException>(() => testDirectory.GetFiles("*", options).Count());
+ }
+ }
+
+ [ConditionalTheory(nameof(CanCreateSymbolicLinks))]
+ [InlineData(false, false)] // OK
+ [InlineData(false, true)] // throw
+ [InlineData(true, false)] // throw, OK on Unix
+ [InlineData(true, true)] // throw
+ public void EnumerateGet_SelfReferencingLink_Static(bool recurse, bool linkAsRoot)
+ {
+ var options = new EnumerationOptions() { RecurseSubdirectories = recurse };
+
+ DirectoryInfo testDirectory = linkAsRoot ?
+ CreateSelfReferencingSymbolicLink() :
+ CreateDirectoryContainingSelfReferencingSymbolicLink();
+
+ // Unix doesn't have a problem when it steps in a self-referencing link through the directory recursion.
+ if ((!recurse || !OperatingSystem.IsWindows()) && !linkAsRoot)
+ {
+ Directory.EnumerateFileSystemEntries(testDirectory.FullName, "*", options).Count();
+ Directory.GetFileSystemEntries(testDirectory.FullName, "*", options).Count();
+
+ Directory.EnumerateDirectories(testDirectory.FullName, "*", options).Count();
+ Directory.GetDirectories(testDirectory.FullName, "*", options).Count();
+
+ Directory.EnumerateFiles(testDirectory.FullName, "*", options).Count();
+ Directory.GetFiles(testDirectory.FullName, "*", options).Count();
+ }
+ else
+ {
+ Assert.Throws<IOException>(() => Directory.EnumerateFileSystemEntries(testDirectory.FullName, "*", options).Count());
+ Assert.Throws<IOException>(() => Directory.GetFileSystemEntries(testDirectory.FullName, "*", options).Count());
+
+ Assert.Throws<IOException>(() => Directory.EnumerateDirectories(testDirectory.FullName, "*", options).Count());
+ Assert.Throws<IOException>(() => Directory.GetDirectories(testDirectory.FullName, "*", options).Count());
+
+ Assert.Throws<IOException>(() => Directory.EnumerateFiles(testDirectory.FullName, "*", options).Count());
+ Assert.Throws<IOException>(() => Directory.GetFiles(testDirectory.FullName, "*", options).Count());
+ }
+ }
}
}
// Positive number: the error code returned by the lstat call
private int _initializedFileCache;
- // The last cached stat information about the file
- // Refresh only collects this if lstat determines the path is a symbolic link
- private Interop.Sys.FileStatus _symlinkCache;
-
- // -1: if the symlink cache isn't initialized - Refresh only changes this value if lstat determines the path is a symbolic link
- // 0: if the symlink cache was initialized with no errors
- // Positive number: the error code returned by the stat call
- private int _initializedSymlinkCache;
-
// We track intent of creation to know whether or not we want to (1) create a
// DirectoryInfo around this status struct or (2) actually are part of a DirectoryInfo.
// Set to true during initialization when the DirectoryEntry's INodeType describes a directory
internal void InvalidateCaches()
{
_initializedFileCache = -1;
- _initializedSymlinkCache = -1;
}
internal bool IsReadOnly(ReadOnlySpan<char> path, bool continueOnError = false)
return _fileCache.Size;
}
- // Tries to refresh the lstat cache (_fileCache) and, if the file is pointing to a symbolic link, then also the stat cache (_symlinkCache)
+ // Tries to refresh the lstat cache (_fileCache).
// 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
internal void RefreshCaches(ReadOnlySpan<char> path)
{
path = Path.TrimEndingDirectorySeparator(path);
// Retrieve the file cache (lstat) to get the details on the object, without following symlinks.
- // If it is a symlink, then subsequently get details on the target of the symlink,
- // storing those results separately. We only report failure if the initial
- // lstat fails, as a broken symlink should still report info on exists, attributes, etc.
+ // If it is a symlink, then subsequently get details on the target of the symlink.
+ // We only report failure if the initial lstat fails, as a broken symlink should still report info on exists, attributes, etc.
if (!TryRefreshFileCache(path))
{
_exists = false;
_isDirectory = CacheHasDirectoryFlag(_fileCache);
// We also need to check if the main path is a symbolic link,
- // in which case, we retrieve the symbolic link data
- if (!_isDirectory && HasSymbolicLinkFlag && TryRefreshSymbolicLinkCache(path))
+ // in which case, we retrieve the symbolic link's target data
+ if (!_isDirectory && HasSymbolicLinkFlag && Interop.Sys.Stat(path, out Interop.Sys.FileStatus target) == 0)
{
// and check again if the symlink path is a directory
- _isDirectory = CacheHasDirectoryFlag(_symlinkCache);
+ _isDirectory = CacheHasDirectoryFlag(target);
}
#if !TARGET_BROWSER
_exists = true;
- // Checks if the specified cache (lstat=_fileCache, stat=_symlinkCache) has the directory attribute set
+ // Checks if the specified cache has the directory attribute set
// Only call if Refresh has been successfully called at least once, and you're
// certain the passed-in cache was successfully retrieved
static bool CacheHasDirectoryFlag(Interop.Sys.FileStatus cache) =>
}
// Checks if the file cache is set to -1 and refreshes it's value.
- // 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.
- // If stat or lstat failed, and continueOnError is set to true, this method will throw.
+ // If it failed, and continueOnError is set to true, this method will throw.
internal void EnsureCachesInitialized(ReadOnlySpan<char> path, bool continueOnError = false)
{
if (_initializedFileCache == -1)
// Throws if any of the caches has an error number saved in it
private void ThrowOnCacheInitializationError(ReadOnlySpan<char> path)
{
- int errno;
// Lstat should always be initialized by Refresh
if (_initializedFileCache != 0)
{
- errno = _initializedFileCache;
- InvalidateCaches();
- throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path));
- }
- // Stat is optionally initialized when Refresh detects object is a symbolic link
- else if (_initializedSymlinkCache != 0 && _initializedSymlinkCache != -1)
- {
- errno = _initializedSymlinkCache;
+ int errno = _initializedFileCache;
InvalidateCaches();
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path));
}
private bool TryRefreshFileCache(ReadOnlySpan<char> path) =>
VerifyStatCall(Interop.Sys.LStat(path, out _fileCache), out _initializedFileCache);
- private bool TryRefreshSymbolicLinkCache(ReadOnlySpan<char> path) =>
- VerifyStatCall(Interop.Sys.Stat(path, out _symlinkCache), out _initializedSymlinkCache);
-
// Receives the return value of a stat or lstat call.
// If the call is unsuccessful, sets the initialized parameter to a positive number representing the last error info.
// If the call is successful, sets the initialized parameter to zero.