diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs index 78390ccdec28c..98fbdd704ff84 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -36,8 +36,10 @@ namespace System.IO.Enumeration // IMPORTANT: Attribute logic must match the logic in FileStatus + entry._status = default; + entry._status.Invalidate(); + bool isDirectory = false; - bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR) { // We know it's a directory. @@ -46,38 +48,49 @@ namespace System.IO.Enumeration // Some operating systems don't have the inode type in the dirent structure, // so we use DT_UNKNOWN as a sentinel value. As such, check if the dirent is a // directory. - else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK - || directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - && Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus targetStatus) >= 0) + else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK || directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) && + entry._status.TryRefreshSecondaryCache(entry.FullPath)) // Only call stat if inode is link or unknown { - // Symlink or unknown: Stat to it to see if we can resolve it to a directory. - isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + // Symlink or unknown: Stat should tell us if we can resolve it to a directory. + isDirectory = entry._status.HasSecondaryDirectoryFlag; } - // Same idea as the directory check, just repeated for (and tweaked due to the - // nature of) symlinks. + + // Same idea as the directory check, just repeated for (and tweaked due to the nature of) symlinks. + bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK) { isSymlink = true; } - else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - && (Interop.Sys.LStat(entry.FullPath, out Interop.Sys.FileStatus linkTargetStatus) >= 0)) + else if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN && + entry._status.TryRefreshMainCache(entry.FullPath)) // Only call lstat if inode is unknown { - isSymlink = (linkTargetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; + isSymlink = entry._status.HasSymbolicLinkFlag; } - entry._status = default; - FileStatus.Initialize(ref entry._status, isDirectory); + entry._status.InitiallyDirectory = isDirectory; FileAttributes attributes = default; if (isSymlink) + { attributes |= FileAttributes.ReparsePoint; + } if (isDirectory) + { attributes |= FileAttributes.Directory; - if (directoryEntry.Name[0] == '.') + } + + // If at this point we have not collected the main cache (lstat) we don't have a hard requirement + // to hit the disk yet, so a soft check is enough to prevent perf regression. + // FileSystemEnumerator.MoveNext may hit the disk, if needed, when updating the attributes. + if (entry.HasHiddenPrefix || (entry._status.IsMainCacheValid && entry._status.HasHiddenFlag)) + { attributes |= FileAttributes.Hidden; + } if (attributes == default) + { attributes = FileAttributes.Normal; + } entry._initialAttributes = attributes; return attributes; @@ -99,6 +112,8 @@ private ReadOnlySpan FullPath } } + private bool HasHiddenPrefix => (_directoryEntry.NameLength > 0 && _directoryEntry.Name[0] == '.'); + public ReadOnlySpan FileName { get @@ -143,12 +158,22 @@ public FileAttributes Attributes public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true); public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true); public bool IsDirectory => _status.InitiallyDirectory; - public bool IsHidden => _directoryEntry.Name[0] == '.'; + public bool IsHidden => + HasHiddenPrefix || _status.IsHidden(FullPath, continueOnError: true); // Need to hit the disk (lstat) to check for flag public FileSystemInfo ToFileSystemInfo() { + Debug.Assert(!PathInternal.IsPartiallyQualified(FullPath), "FullPath should be fully qualified when constructed from directory enumeration"); + string fullPath = ToFullPath(); - return FileSystemInfo.Create(fullPath, new string(FileName), ref _status); + string fileName = new(FileName); + + FileSystemInfo info = IsDirectory + ? new DirectoryInfo(fullPath, fileName: fileName, isNormalized: true) + : new FileInfo(fullPath, fileName: fileName, isNormalized: true); + + info.Init(ref _status); + return info; } /// diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs index 45b0ed8b04f8d..84013dc771ad5 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs @@ -9,227 +9,316 @@ internal struct FileStatus { private const int NanosecondsPerTick = 100; - // The last cached stat information about the file - private Interop.Sys.FileStatus _fileStatus; + // Exists as of the last refresh + private bool _exists; - // -1 if _fileStatus isn't initialized, 0 if _fileStatus was initialized with no - // errors, or the errno error code. - private int _fileStatusInitialized; + // The last cached lstat information about the file + private Interop.Sys.FileStatus _mainCache; - // 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. - internal bool InitiallyDirectory { get; private set; } + // 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 _secondaryCache; - // Is a directory as of the last refresh - internal bool _isDirectory; + // -1 if _mainCache isn't initialized - Refresh should always change this value + // 0 if _mainCache was initialized with no errors + // or the errno error code (always positive value) + private int _initializedMainCache; - // Exists as of the last refresh - private bool _exists; + // -1 if _secondaryCache isn't initialized - Refresh only changes this value if lstat determines the path is a symbolic link + // 0 if _secondaryCache was initialized with no errors + // or the errno error code (always positive value) + private int _initializedSecondaryCache; - internal static void Initialize( - ref FileStatus status, - bool isDirectory) - { - status.InitiallyDirectory = isDirectory; - status._fileStatusInitialized = -1; - } + // Is a directory as of the last refresh + private bool _isDirectory; - internal void Invalidate() => _fileStatusInitialized = -1; + // Checks if the main path (without following symbolic links) has the hidden attribute + // Only call if Refresh has been successfully called at least once + internal bool HasHiddenFlag => + (_mainCache.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN; - internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) + // Checks if the main path (without following symbolic links) has the read-only attribute + // Only call if Refresh has been successfully called at least once + private bool HasReadOnlyFlag { - EnsureStatInitialized(path, continueOnError); + get + { #if TARGET_BROWSER - const Interop.Sys.Permissions readBit = Interop.Sys.Permissions.S_IRUSR; - const Interop.Sys.Permissions writeBit = Interop.Sys.Permissions.S_IWUSR; + const Interop.Sys.Permissions readBit = Interop.Sys.Permissions.S_IRUSR; + const Interop.Sys.Permissions writeBit = Interop.Sys.Permissions.S_IWUSR; #else - Interop.Sys.Permissions readBit, writeBit; + Interop.Sys.Permissions readBit, writeBit; - if (_fileStatus.Uid == Interop.Sys.GetEUid()) - { - // User effectively owns the file - readBit = Interop.Sys.Permissions.S_IRUSR; - writeBit = Interop.Sys.Permissions.S_IWUSR; + if (_mainCache.Uid == Interop.Sys.GetEUid()) + { + // User effectively owns the file + readBit = Interop.Sys.Permissions.S_IRUSR; + writeBit = Interop.Sys.Permissions.S_IWUSR; + } + else if (_mainCache.Gid == Interop.Sys.GetEGid()) + { + // User belongs to a group that effectively owns the file + readBit = Interop.Sys.Permissions.S_IRGRP; + writeBit = Interop.Sys.Permissions.S_IWGRP; + } + else + { + // Others permissions + readBit = Interop.Sys.Permissions.S_IROTH; + writeBit = Interop.Sys.Permissions.S_IWOTH; + } + #endif + return ((_mainCache.Mode & (int)readBit) != 0 && // has read permission + (_mainCache.Mode & (int)writeBit) == 0); // but not write permission } - else if (_fileStatus.Gid == Interop.Sys.GetEGid()) + } + + internal bool HasSecondaryDirectoryFlag => HasDirectoryFlag(_secondaryCache); + + // Checks if the main path is a symbolic link + // Only call if Refresh has been successfully called at least once + internal bool HasSymbolicLinkFlag => + (_mainCache.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; + + // 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. + internal bool InitiallyDirectory { get; set; } + + internal bool IsMainCacheValid => _initializedMainCache == 0; + + internal bool IsSecondaryCacheValid => _initializedSecondaryCache == 0; + + private bool IsValid => + _initializedMainCache == 0 && // Should always be successfully refreshed + (_initializedSecondaryCache == -1 || _initializedSecondaryCache == 0); // Only refreshed when path is detected to be a symbolic link + + internal void EnsureCachesInitialized(ReadOnlySpan path, bool continueOnError = false) + { + if (!IsValid) { - // User belongs to a group that effectively owns the file - readBit = Interop.Sys.Permissions.S_IRGRP; - writeBit = Interop.Sys.Permissions.S_IWGRP; + RefreshCaches(path); } - else + + if (!continueOnError) { - // Others permissions - readBit = Interop.Sys.Permissions.S_IROTH; - writeBit = Interop.Sys.Permissions.S_IWOTH; + ThrowOnCacheInitializationError(path); } -#endif - - return ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission - (_fileStatus.Mode & (int)writeBit) == 0); // but not write permission } - public FileAttributes GetAttributes(ReadOnlySpan path, ReadOnlySpan fileName) + internal FileAttributes GetAttributes(ReadOnlySpan path, ReadOnlySpan fileName) { // IMPORTANT: Attribute logic must match the logic in FileSystemEntry - EnsureStatInitialized(path); + EnsureCachesInitialized(path); if (!_exists) + { return (FileAttributes)(-1); + } FileAttributes attributes = default; - if (IsReadOnly(path)) + if (HasReadOnlyFlag) + { attributes |= FileAttributes.ReadOnly; + } - if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK) + if (HasSymbolicLinkFlag) + { attributes |= FileAttributes.ReparsePoint; + } if (_isDirectory) + { attributes |= FileAttributes.Directory; + } // If the filename starts with a period or has UF_HIDDEN flag set, it's hidden. - if (fileName.Length > 0 && (fileName[0] == '.' || (_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN)) + if (fileName.Length > 0 && (fileName[0] == '.' || HasHiddenFlag)) + { attributes |= FileAttributes.Hidden; + } return attributes != default ? attributes : FileAttributes.Normal; } - public void SetAttributes(string path, FileAttributes attributes) + internal DateTimeOffset GetCreationTime(ReadOnlySpan path, bool continueOnError = false) { - // Validate that only flags from the attribute are being provided. This is an - // approximation for the validation done by the Win32 function. - const FileAttributes allValidFlags = - FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Device | - FileAttributes.Directory | FileAttributes.Encrypted | FileAttributes.Hidden | - FileAttributes.IntegrityStream | FileAttributes.Normal | FileAttributes.NoScrubData | - FileAttributes.NotContentIndexed | FileAttributes.Offline | FileAttributes.ReadOnly | - FileAttributes.ReparsePoint | FileAttributes.SparseFile | FileAttributes.System | - FileAttributes.Temporary; - if ((attributes & ~allValidFlags) != 0) - { - // Using constant string for argument to match historical throw - throw new ArgumentException(SR.Arg_InvalidFileAttrs, "Attributes"); - } - - EnsureStatInitialized(path); - + EnsureCachesInitialized(path, continueOnError); if (!_exists) - FileSystemInfo.ThrowNotFound(path); - - if (Interop.Sys.CanSetHiddenFlag) { - if ((attributes & FileAttributes.Hidden) != 0) - { - if ((_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == 0) - { - // If Hidden flag is set and cached file status does not have the flag set then set it - Interop.CheckIo(Interop.Sys.LChflags(path, (_fileStatus.UserFlags | (uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory); - } - } - else - { - if ((_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN) - { - // If Hidden flag is not set and cached file status does have the flag set then remove it - Interop.CheckIo(Interop.Sys.LChflags(path, (_fileStatus.UserFlags & ~(uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory); - } - } + return DateTimeOffset.FromFileTime(0); } - // The only thing we can reasonably change is whether the file object is readonly by changing permissions. - - int newMode = _fileStatus.Mode; - if ((attributes & FileAttributes.ReadOnly) != 0) - { - // Take away all write permissions from user/group/everyone - newMode &= ~(int)(Interop.Sys.Permissions.S_IWUSR | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IWOTH); - } - else if ((newMode & (int)Interop.Sys.Permissions.S_IRUSR) != 0) + if ((_mainCache.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0) { - // Give write permission to the owner if the owner has read permission - newMode |= (int)Interop.Sys.Permissions.S_IWUSR; + return UnixTimeToDateTimeOffset(_mainCache.BirthTime, _mainCache.BirthTimeNsec); } - // Change the permissions on the file - if (newMode != _fileStatus.Mode) + // fall back to the oldest time we have in between change and modify time + if (_mainCache.MTime < _mainCache.CTime || + (_mainCache.MTime == _mainCache.CTime && _mainCache.MTimeNsec < _mainCache.CTimeNsec)) { - Interop.CheckIo(Interop.Sys.ChMod(path, newMode), path, InitiallyDirectory); + return UnixTimeToDateTimeOffset(_mainCache.MTime, _mainCache.MTimeNsec); } - _fileStatusInitialized = -1; + return UnixTimeToDateTimeOffset(_mainCache.CTime, _mainCache.CTimeNsec); } + internal bool GetExists(ReadOnlySpan path) { - if (_fileStatusInitialized == -1) - Refresh(path); - + EnsureCachesInitialized(path, continueOnError: true); return _exists && InitiallyDirectory == _isDirectory; } - internal DateTimeOffset GetCreationTime(ReadOnlySpan path, bool continueOnError = false) + internal DateTimeOffset GetLastAccessTime(ReadOnlySpan path, bool continueOnError = false) { - EnsureStatInitialized(path, continueOnError); + EnsureCachesInitialized(path, continueOnError); if (!_exists) + { return DateTimeOffset.FromFileTime(0); + } + return UnixTimeToDateTimeOffset(_mainCache.ATime, _mainCache.ATimeNsec); + } - if ((_fileStatus.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0) - return UnixTimeToDateTimeOffset(_fileStatus.BirthTime, _fileStatus.BirthTimeNsec); + internal long GetLength(ReadOnlySpan path, bool continueOnError = false) + { + EnsureCachesInitialized(path, continueOnError); + return _mainCache.Size; + } - // fall back to the oldest time we have in between change and modify time - if (_fileStatus.MTime < _fileStatus.CTime || - (_fileStatus.MTime == _fileStatus.CTime && _fileStatus.MTimeNsec < _fileStatus.CTimeNsec)) - return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec); + // Checks if the specified stat or lstat cache has the directory attribute + // Only call if Refresh has been successfully called at least once and + // you're certain the passed-in cache was successfully retrieved + private static bool HasDirectoryFlag(Interop.Sys.FileStatus cache) => + (cache.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - return UnixTimeToDateTimeOffset(_fileStatus.CTime, _fileStatus.CTimeNsec); + internal void Invalidate() + { + _initializedMainCache = -1; + _initializedSecondaryCache = -1; } - internal void SetCreationTime(string path, DateTimeOffset time) + internal bool IsDirectory(ReadOnlySpan path, bool continueOnError = false) { - // Unix provides APIs to update the last access time (atime) and last modification time (mtime). - // There is no API to update the CreationTime. - // Some platforms (e.g. Linux) don't store a creation time. On those platforms, the creation time - // is synthesized as the oldest of last status change time (ctime) and last modification time (mtime). - // We update the LastWriteTime (mtime). - // This triggers a metadata change for FileSystemWatcher NotifyFilters.CreationTime. - // Updating the mtime, causes the ctime to be set to 'now'. So, on platforms that don't store a - // CreationTime, GetCreationTime will return the value that was previously set (when that value - // wasn't in the future). - SetLastWriteTime(path, time); + // We first check if main path has directory flag, then follow the symbolic link in case the target has directory flag + // So we need to try to refresh both caches + EnsureCachesInitialized(path, continueOnError); + return _isDirectory; // Value should be set in Refresh } - internal DateTimeOffset GetLastAccessTime(ReadOnlySpan path, bool continueOnError = false) + internal bool IsHidden(ReadOnlySpan path, bool continueOnError = false) { - EnsureStatInitialized(path, continueOnError); - if (!_exists) - return DateTimeOffset.FromFileTime(0); - return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec); + // We only check for the hidden flag in the main path without following symbolic links + if (!IsMainCacheValid) + { + if (!TryRefreshMainCache(path)) + { + if (!continueOnError) + { + ThrowOnCacheInitializationError(path); + } + } + } + return HasHiddenFlag; } - internal void SetLastAccessTime(string path, DateTimeOffset time) => SetAccessOrWriteTime(path, time, isAccessTime: true); + internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) + { + // We only check for the readonly flag in the main path without following symbolic links + if (!IsMainCacheValid) + { + if (!TryRefreshMainCache(path)) + { + if (!continueOnError) + { + ThrowOnCacheInitializationError(path); + } + } + } + return HasReadOnlyFlag; + } - internal DateTimeOffset GetLastWriteTime(ReadOnlySpan path, bool continueOnError = false) + internal bool IsSymbolicLink(ReadOnlySpan path, bool continueOnError = false) { - EnsureStatInitialized(path, continueOnError); - if (!_exists) - return DateTimeOffset.FromFileTime(0); - return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec); + // We only check for the symbolic link flag in the main path without following symbolic links + if (!IsMainCacheValid) + { + if (!TryRefreshMainCache(path)) + { + if (!continueOnError) + { + ThrowOnCacheInitializationError(path); + } + } + } + return HasSymbolicLinkFlag; } - internal void SetLastWriteTime(string path, DateTimeOffset time) => SetAccessOrWriteTime(path, time, isAccessTime: false); + internal void RefreshCaches(ReadOnlySpan path) + { + // This should not throw, instead we store the result so that we can throw it + // when someone actually accesses a property. - private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds) + _isDirectory = false; + path = Path.TrimEndingDirectorySeparator(path); + + // Use 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 (!TryRefreshMainCache(path)) + { + _exists = false; + return; + } + + _isDirectory = HasDirectoryFlag(_mainCache); + + if (HasSymbolicLinkFlag) + { + if (TryRefreshSecondaryCache(path)) + { + _isDirectory = HasDirectoryFlag(_secondaryCache); + } + } + + _exists = true; + } + private void ThrowOnCacheInitializationError(ReadOnlySpan path) { - return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick).ToLocalTime(); + int errno = 0; + + // Lstat should always be initialized by Refresh + if (_initializedMainCache != 0) + { + errno = _initializedMainCache; + } + // Stat is optionally initialized when Refresh detects object is a symbolic link + else if (_initializedSecondaryCache != 0 && _initializedSecondaryCache != -1) + { + errno = _initializedSecondaryCache; + } + + if (errno != 0) + { + Invalidate(); + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path)); + } } + internal bool TryRefreshMainCache(ReadOnlySpan path) => + VerifyStatCall(Interop.Sys.LStat(path, out _mainCache), out _initializedMainCache); + + internal bool TryRefreshSecondaryCache(ReadOnlySpan path) => + VerifyStatCall(Interop.Sys.Stat(path, out _secondaryCache), out _initializedSecondaryCache); + private unsafe void SetAccessOrWriteTime(string path, DateTimeOffset time, bool isAccessTime) { // force a refresh so that we have an up-to-date times for values not being overwritten - _fileStatusInitialized = -1; - EnsureStatInitialized(path); + Invalidate(); + EnsureCachesInitialized(path); // we use utimes()/utimensat() to set the accessTime and writeTime Interop.Sys.TimeSpec* buf = stackalloc Interop.Sys.TimeSpec[2]; @@ -250,87 +339,144 @@ private unsafe void SetAccessOrWriteTime(string path, DateTimeOffset time, bool { buf[0].TvSec = seconds; buf[0].TvNsec = nanoseconds; - buf[1].TvSec = _fileStatus.MTime; - buf[1].TvNsec = _fileStatus.MTimeNsec; + buf[1].TvSec = _mainCache.MTime; + buf[1].TvNsec = _mainCache.MTimeNsec; } else { - buf[0].TvSec = _fileStatus.ATime; - buf[0].TvNsec = _fileStatus.ATimeNsec; + buf[0].TvSec = _mainCache.ATime; + buf[0].TvNsec = _mainCache.ATimeNsec; buf[1].TvSec = seconds; buf[1].TvNsec = nanoseconds; } #endif Interop.CheckIo(Interop.Sys.UTimensat(path, buf), path, InitiallyDirectory); - _fileStatusInitialized = -1; + Invalidate(); } - internal long GetLength(ReadOnlySpan path, bool continueOnError = false) + internal void SetAttributes(string path, FileAttributes attributes) { - EnsureStatInitialized(path, continueOnError); - return _fileStatus.Size; - } + // Validate that only flags from the attribute are being provided. This is an + // approximation for the validation done by the Win32 function. + const FileAttributes allValidFlags = + FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Device | + FileAttributes.Directory | FileAttributes.Encrypted | FileAttributes.Hidden | + FileAttributes.IntegrityStream | FileAttributes.Normal | FileAttributes.NoScrubData | + FileAttributes.NotContentIndexed | FileAttributes.Offline | FileAttributes.ReadOnly | + FileAttributes.ReparsePoint | FileAttributes.SparseFile | FileAttributes.System | + FileAttributes.Temporary; + if ((attributes & ~allValidFlags) != 0) + { + // Using constant string for argument to match historical throw + throw new ArgumentException(SR.Arg_InvalidFileAttrs, "Attributes"); + } - public void Refresh(ReadOnlySpan path) - { - // This should not throw, instead we store the result so that we can throw it - // when someone actually accesses a property. + EnsureCachesInitialized(path); - // Use 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. - _isDirectory = false; - path = Path.TrimEndingDirectorySeparator(path); - - int result = Interop.Sys.LStat(path, out _fileStatus); - if (result < 0) + if (!_exists) { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + FileSystemInfo.ThrowNotFound(path); + } - // This should never set the error if the file can't be found. - // (see the Windows refresh passing returnErrorOnNotFound: false). - if (errorInfo.Error == Interop.Error.ENOENT - || errorInfo.Error == Interop.Error.ENOTDIR) + if (Interop.Sys.CanSetHiddenFlag) + { + bool isHidden = HasHiddenFlag; + if ((attributes & FileAttributes.Hidden) != 0) { - _fileStatusInitialized = 0; - _exists = false; + if (!isHidden) + { + // If Hidden flag is set and cached file status does not have the flag set then set it + Interop.CheckIo(Interop.Sys.LChflags(path, (_mainCache.UserFlags | (uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory); + } } else { - _fileStatusInitialized = errorInfo.RawErrno; + if (isHidden) + { + // If Hidden flag is not set and cached file status does have the flag set then remove it + Interop.CheckIo(Interop.Sys.LChflags(path, (_mainCache.UserFlags & ~(uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory); + } } - return; } - _exists = true; + // The only thing we can reasonably change is whether the file object is readonly by changing permissions. - // IMPORTANT: Is directory logic must match the logic in FileSystemEntry - _isDirectory = (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + int newMode = _mainCache.Mode; + if ((attributes & FileAttributes.ReadOnly) != 0) + { + // Take away all write permissions from user/group/everyone + newMode &= ~(int)(Interop.Sys.Permissions.S_IWUSR | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IWOTH); + } + else if ((newMode & (int)Interop.Sys.Permissions.S_IRUSR) != 0) + { + // Give write permission to the owner if the owner has read permission + newMode |= (int)Interop.Sys.Permissions.S_IWUSR; + } - // If we're a symlink, attempt to check the target to see if it is a directory - if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK && - Interop.Sys.Stat(path, out Interop.Sys.FileStatus targetStatus) >= 0) + // Change the permissions on the file + if (newMode != _mainCache.Mode) { - _isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + Interop.CheckIo(Interop.Sys.ChMod(path, newMode), path, InitiallyDirectory); } - _fileStatusInitialized = 0; + Invalidate(); } - internal void EnsureStatInitialized(ReadOnlySpan path, bool continueOnError = false) + internal void SetCreationTime(string path, DateTimeOffset time) => + // Unix provides APIs to update the last access time (atime) and last modification time (mtime). + // There is no API to update the CreationTime. + // Some platforms (e.g. Linux) don't store a creation time. On those platforms, the creation time + // is synthesized as the oldest of last status change time (ctime) and last modification time (mtime). + // We update the LastWriteTime (mtime). + // This triggers a metadata change for FileSystemWatcher NotifyFilters.CreationTime. + // Updating the mtime, causes the ctime to be set to 'now'. So, on platforms that don't store a + // CreationTime, GetCreationTime will return the value that was previously set (when that value + // wasn't in the future). + SetLastWriteTime(path, time); + + internal void SetLastAccessTime(string path, DateTimeOffset time) => + SetAccessOrWriteTime(path, time, isAccessTime: true); + + internal DateTimeOffset GetLastWriteTime(ReadOnlySpan path, bool continueOnError = false) { - if (_fileStatusInitialized == -1) + EnsureCachesInitialized(path, continueOnError); + if (!_exists) { - Refresh(path); + return DateTimeOffset.FromFileTime(0); } + return UnixTimeToDateTimeOffset(_mainCache.MTime, _mainCache.MTimeNsec); + } - if (_fileStatusInitialized != 0 && !continueOnError) + internal void SetLastWriteTime(string path, DateTimeOffset time) => + SetAccessOrWriteTime(path, time, isAccessTime: false); + + // Receives the return value of a stat or lstat call. + // If the call is unsuccessful, returns a positive number representing the last error info. + // If the call is successful, returns 0. + private bool VerifyStatCall(int returnValue, out int initialized) + { + initialized = 0; + + // stat and lstat return -1 on error, 0 on success + if (returnValue < 0) { - int errno = _fileStatusInitialized; - _fileStatusInitialized = -1; - throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path)); + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + + // This should never set the error if the file can't be found. + // (see the Windows refresh passing returnErrorOnNotFound: false). + if (errorInfo.Error != Interop.Error.ENOENT && // A component of the path does not exist, or path is an empty string + errorInfo.Error != Interop.Error.ENOTDIR) // A component of the path prefix of path is not a directory + { + // Expect a positive integer + initialized = errorInfo.RawErrno; + } + return false; } + + return true; } + + private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds) => + DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick).ToLocalTime(); } } diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs index 24218a8e4a6a7..dfeba4144a675 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs @@ -11,19 +11,8 @@ public partial class FileSystemInfo protected FileSystemInfo() { - FileStatus.Initialize(ref _fileStatus, this is DirectoryInfo); - } - - internal static unsafe FileSystemInfo Create(string fullPath, string fileName, ref FileStatus fileStatus) - { - FileSystemInfo info = fileStatus.InitiallyDirectory - ? (FileSystemInfo)new DirectoryInfo(fullPath, fileName: fileName, isNormalized: true) - : new FileInfo(fullPath, fileName: fileName, isNormalized: true); - - Debug.Assert(!PathInternal.IsPartiallyQualified(fullPath), $"'{fullPath}' should be fully qualified when constructed from directory enumeration"); - - info.Init(ref fileStatus); - return info; + _fileStatus.Invalidate(); + _fileStatus.InitiallyDirectory = this is DirectoryInfo; } internal void Invalidate() => _fileStatus.Invalidate(); @@ -31,7 +20,7 @@ internal static unsafe FileSystemInfo Create(string fullPath, string fileName, r internal unsafe void Init(ref FileStatus fileStatus) { _fileStatus = fileStatus; - _fileStatus.EnsureStatInitialized(FullPath); + _fileStatus.EnsureCachesInitialized(FullPath); } public FileAttributes Attributes @@ -62,7 +51,7 @@ internal DateTimeOffset LastWriteTimeCore internal long LengthCore => _fileStatus.GetLength(FullPath); - public void Refresh() => _fileStatus.Refresh(FullPath); + public void Refresh() => _fileStatus.RefreshCaches(FullPath); internal static void ThrowNotFound(string path) { diff --git a/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs b/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs index 3736c0669cf78..eba349e849a03 100644 --- a/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs @@ -113,18 +113,41 @@ public void DirectoryAttributesAreExpected() } [Fact] - public void IsHiddenAttribute() + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + public void IsHiddenAttribute_Windows_OSX() { - DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); - FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + // Windows and MacOS hide a file by setting the hidden attribute + IsHiddenAttributeInternal(useDotPrefix: false, useHiddenFlag: true); + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void IsHiddenAttribute_Unix() + { // Put a period in front to make it hidden on Unix - FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, "." + GetTestFileName())); + IsHiddenAttributeInternal(useDotPrefix: true, useHiddenFlag: false); + } + + private void IsHiddenAttributeInternal(bool useDotPrefix, bool useHiddenFlag) + { + string prefix = useDotPrefix ? "." : ""; + + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, prefix + GetTestFileName())); fileOne.Create().Dispose(); fileTwo.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileTwo.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + + if (useHiddenFlag) + { + fileTwo.Attributes |= FileAttributes.Hidden; + } + + FileInfo fileCheck = new FileInfo(fileTwo.FullName); + Assert.Equal(fileTwo.Attributes, fileCheck.Attributes); IEnumerable enumerable = new FileSystemEnumerable( testDirectory.FullName, @@ -136,5 +159,29 @@ public void IsHiddenAttribute() Assert.Equal(new string[] { fileTwo.FullName }, enumerable); } + + [Fact] + public void IsReadOnlyAttribute() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + + fileOne.Create().Dispose(); + fileTwo.Create().Dispose(); + + fileTwo.Attributes |= FileAttributes.ReadOnly; + + IEnumerable enumerable = new FileSystemEnumerable( + testDirectory.FullName, + (ref FileSystemEntry entry) => entry.ToFullPath(), + new EnumerationOptions() { AttributesToSkip = 0 }) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => (entry.Attributes & FileAttributes.ReadOnly) != 0 + }; + + Assert.Equal(new string[] { fileTwo.FullName }, enumerable); + } } -} +} \ No newline at end of file diff --git a/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs b/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs index 3a22c61b6f77e..19c40e9c6a0b5 100644 --- a/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs @@ -21,25 +21,42 @@ protected virtual string[] GetPaths(string directory, EnumerationOptions options } [Fact] - public void SkippingHiddenFiles() + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + public void SkippingHiddenFiles_Windows_OSX() + { + SkippingHiddenFilesInternal(useDotPrefix: false, useHiddenFlag: true); + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void SkippingHiddenFiles_Unix() + { + SkippingHiddenFilesInternal(useDotPrefix: true, useHiddenFlag: false); + } + + private void SkippingHiddenFilesInternal(bool useDotPrefix, bool useHiddenFlag) { DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); DirectoryInfo testSubdirectory = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, GetTestFileName())); - FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); - // Put a period in front to make it hidden on Unix - FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, "." + GetTestFileName())); + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); FileInfo fileThree = new FileInfo(Path.Combine(testSubdirectory.FullName, GetTestFileName())); - FileInfo fileFour = new FileInfo(Path.Combine(testSubdirectory.FullName, "." + GetTestFileName())); + + // Put a period in front of files two and four to make them hidden on Unix + string prefix = useDotPrefix ? "." : ""; + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, prefix + GetTestFileName())); + FileInfo fileFour = new FileInfo(Path.Combine(testSubdirectory.FullName, prefix + GetTestFileName())); fileOne.Create().Dispose(); fileTwo.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileTwo.Attributes = fileTwo.Attributes | FileAttributes.Hidden; fileThree.Create().Dispose(); fileFour.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileFour.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + + if (useHiddenFlag) + { + fileTwo.Attributes |= FileAttributes.Hidden; + fileFour.Attributes |= FileAttributes.Hidden; + } // Default EnumerationOptions is to skip hidden string[] paths = GetPaths(testDirectory.FullName, new EnumerationOptions()); @@ -103,4 +120,4 @@ protected override string[] GetPaths(string directory, EnumerationOptions option return new DirectoryInfo(directory).GetFiles("*", options).Select(i => i.FullName).ToArray(); } } -} +} \ No newline at end of file