// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace System.IO { internal partial struct FileStatus { private const int NanosecondsPerTick = 100; private const int InitializedExistsBrokenLink = -5; // target is link with no target. private const int InitializedExistsDir = -4; // target is directory. private const int InitializedExistsFile = -3; // target is file. private const int InitializedNotExistsNotADir = -2; // entry parent path is not a dir. private const int InitializedNotExists = -1; // entry does not exist. private const int Uninitialized = 0; // uninitialized, '0' to make default(FileStatus) uninitialized. // Tracks the initialization state. // < 0 : initialized succesfully. Value is one of the Initialized* consts. // 0 : uninitialized. // > 0 : initialized with error. Value is raw errno. private int _state; // The last cached lstat information about the file. // Must only be used after calling EnsureCachesInitialized and checking EntryExists is true. private Interop.Sys.FileStatus _fileCache; private bool EntryExists => _state <= InitializedExistsFile; private bool IsDir => _state == InitializedExistsDir; private bool IsBrokenLink => _state == InitializedExistsBrokenLink; // Check if the main path (without following symlinks) has the hidden attribute set. private bool HasHiddenFlag { get { Debug.Assert(_state != Uninitialized); // Use this after EnsureCachesInitialized has been called. return EntryExists && (_fileCache.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN; } } // Checks if the main path (without following symlinks) has the read-only attribute set. private bool HasReadOnlyFlag { get { Debug.Assert(_state != Uninitialized); // Use this after EnsureCachesInitialized has been called. if (!EntryExists || IsBrokenLink) { return false; } #if TARGET_BROWSER || TARGET_WASI var mode = ((UnixFileMode)_fileCache.Mode & FileSystem.ValidUnixFileModes); bool isUserReadOnly = (mode & UnixFileMode.UserRead) != 0 && // has read permission (mode & UnixFileMode.UserWrite) == 0; // but not write permission return isUserReadOnly; #else if (_isReadOnlyCache == 0) { return false; } else if (_isReadOnlyCache == 1) { return true; } else { bool isReadOnly = IsModeReadOnlyCore(); _isReadOnlyCache = isReadOnly ? 1 : 0; return isReadOnly; } #endif } } #if !TARGET_BROWSER && !TARGET_WASI // HasReadOnlyFlag cache. // Must only be used after calling EnsureCachesInitialized. private int _isReadOnlyCache; private bool IsModeReadOnlyCore() { var mode = ((UnixFileMode)_fileCache.Mode & FileSystem.ValidUnixFileModes); bool isUserReadOnly = (mode & UnixFileMode.UserRead) != 0 && // has read permission (mode & UnixFileMode.UserWrite) == 0; // but not write permission bool isGroupReadOnly = (mode & UnixFileMode.GroupRead) != 0 && // has read permission (mode & UnixFileMode.GroupWrite) == 0; // but not write permission bool isOtherReadOnly = (mode & UnixFileMode.OtherRead) != 0 && // has read permission (mode & UnixFileMode.OtherWrite) == 0; // but not write permission // If they are all the same, no need to check user/group. if ((isUserReadOnly == isGroupReadOnly) && (isGroupReadOnly == isOtherReadOnly)) { return isUserReadOnly; } if (_fileCache.Uid == Interop.Sys.GetEUid()) { // User owns the file. return isUserReadOnly; } // System files often have the same permissions for group and other (umask 022). if (isGroupReadOnly == isUserReadOnly) { return isGroupReadOnly; } if (Interop.Sys.IsMemberOfGroup(_fileCache.Gid)) { // User belongs to group that owns the file. return isGroupReadOnly; } else { // Other permissions. return isOtherReadOnly; } } #endif // Checks if the main path is a symbolic link private bool HasSymbolicLinkFlag { get { Debug.Assert(_state != Uninitialized); // Use this after EnsureCachesInitialized has been called. return EntryExists && (_fileCache.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; } } // Sets the cache initialization flags to 0, which means the caches are now uninitialized internal void InvalidateCaches() { _state = Uninitialized; } internal bool IsReadOnly(ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(path, continueOnError); return HasReadOnlyFlag; } internal bool IsFileSystemEntryHidden(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName) { // Because this is called for FileSystemEntry we can assume the entry exists and // avoid initialization in some cases. if (IsNameHidden(fileName)) { return true; } if (!Interop.Sys.SupportsHiddenFlag) { return false; } EnsureCachesInitialized(path, continueOnError: true); return HasHiddenFlag; } internal static bool IsNameHidden(ReadOnlySpan<char> fileName) => fileName.StartsWith('.'); // Returns true if the path points to a directory, or if the path is a symbolic link // that points to a directory internal bool IsDirectory(ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(path, continueOnError); return IsDir; } internal bool IsSymbolicLink(ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(path, continueOnError); return HasSymbolicLinkFlag; } internal FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName, bool continueOnError = false) => GetAttributes(handle: null, path, fileName, continueOnError); internal FileAttributes GetAttributes(SafeFileHandle handle, bool continueOnError = false) => GetAttributes(handle, handle.Path, Path.GetFileName(handle.Path), continueOnError); private FileAttributes GetAttributes(SafeFileHandle? handle, ReadOnlySpan<char> path, ReadOnlySpan<char> fileName, bool continueOnError = false) { Debug.Assert(handle is not null || path.Length > 0); EnsureCachesInitialized(handle, path, continueOnError); if (!EntryExists) return (FileAttributes)(-1); FileAttributes attributes = default; if (HasReadOnlyFlag) attributes |= FileAttributes.ReadOnly; if (HasSymbolicLinkFlag) attributes |= FileAttributes.ReparsePoint; if (IsDir) // Refresh caches this attributes |= FileAttributes.Directory; if (IsNameHidden(fileName) || HasHiddenFlag) attributes |= FileAttributes.Hidden; return attributes != default ? attributes : FileAttributes.Normal; } internal void SetAttributes(string path, FileAttributes attributes, bool asDirectory) => SetAttributes(handle: null, path, attributes, asDirectory); internal void SetAttributes(SafeFileHandle handle, FileAttributes attributes, bool asDirectory) => SetAttributes(handle, handle.Path, attributes, asDirectory); private void SetAttributes(SafeFileHandle? handle, string? path, FileAttributes attributes, bool asDirectory) { // 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"); } EnsureCachesInitialized(handle, path); if (!EntryExists) ThrowNotFound(path); if (Interop.Sys.CanSetHiddenFlag) { bool hidden = (attributes & FileAttributes.Hidden) != 0; if (hidden ^ HasHiddenFlag) { uint flags = hidden ? _fileCache.UserFlags | (uint)Interop.Sys.UserFlags.UF_HIDDEN : _fileCache.UserFlags & ~(uint)Interop.Sys.UserFlags.UF_HIDDEN; int rv = handle is not null ? Interop.Sys.FChflags(handle, flags) : Interop.Sys.LChflags(path!, flags); Interop.CheckIo(rv, path, asDirectory); } } // The only thing we can reasonably change is whether the file object is readonly by changing permissions. int oldMode = _fileCache.Mode & (int)FileSystem.ValidUnixFileModes; int newMode = oldMode; if ((attributes & FileAttributes.ReadOnly) != 0) { // Take away all write permissions from user/group/everyone newMode &= ~(int)(UnixFileMode.UserWrite | UnixFileMode.GroupWrite | UnixFileMode.OtherWrite); } else if ((newMode & (int)UnixFileMode.UserRead) != 0) { // Give write permission to the owner if the owner has read permission newMode |= (int)UnixFileMode.UserWrite; } // Change the permissions on the file if (newMode != oldMode) { int rv = handle is not null ? Interop.Sys.FChMod(handle, newMode) : Interop.Sys.ChMod(path!, newMode); Interop.CheckIo(rv, path, asDirectory); } InvalidateCaches(); } internal bool GetExists(ReadOnlySpan<char> path, bool asDirectory) { EnsureCachesInitialized(path, continueOnError: true); return EntryExists && asDirectory == IsDir; } internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path, bool continueOnError = false) => GetCreationTime(handle: null, path, continueOnError); internal DateTimeOffset GetCreationTime(SafeFileHandle handle, bool continueOnError = false) => GetCreationTime(handle, handle.Path, continueOnError); private DateTimeOffset GetCreationTime(SafeFileHandle? handle, ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(handle, path, continueOnError); if (!EntryExists) return new DateTimeOffset(DateTime.FromFileTimeUtc(0)); if ((_fileCache.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0) return UnixTimeToDateTimeOffset(_fileCache.BirthTime, _fileCache.BirthTimeNsec); // fall back to the oldest time we have in between change and modify time if (_fileCache.MTime < _fileCache.CTime || (_fileCache.MTime == _fileCache.CTime && _fileCache.MTimeNsec < _fileCache.CTimeNsec)) return UnixTimeToDateTimeOffset(_fileCache.MTime, _fileCache.MTimeNsec); return UnixTimeToDateTimeOffset(_fileCache.CTime, _fileCache.CTimeNsec); } internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path, bool continueOnError = false) => GetLastAccessTime(handle: null, path, continueOnError); internal DateTimeOffset GetLastAccessTime(SafeFileHandle handle, bool continueOnError = false) => GetLastAccessTime(handle, handle.Path, continueOnError); private DateTimeOffset GetLastAccessTime(SafeFileHandle? handle, ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(handle, path, continueOnError); if (!EntryExists) return new DateTimeOffset(DateTime.FromFileTimeUtc(0)); return UnixTimeToDateTimeOffset(_fileCache.ATime, _fileCache.ATimeNsec); } internal void SetLastAccessTime(string path, DateTimeOffset time, bool asDirectory) => SetLastAccessTime(handle: null, path, time, asDirectory); internal void SetLastAccessTime(SafeFileHandle handle, DateTimeOffset time, bool asDirectory) => SetLastAccessTime(handle, handle.Path, time, asDirectory); private void SetLastAccessTime(SafeFileHandle? handle, string? path, DateTimeOffset time, bool asDirectory) => SetAccessOrWriteTime(handle, path, time, isAccessTime: true, asDirectory); internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path, bool continueOnError = false) => GetLastWriteTime(handle: null, path, continueOnError); internal DateTimeOffset GetLastWriteTime(SafeFileHandle handle, bool continueOnError = false) => GetLastWriteTime(handle, handle.Path, continueOnError); private DateTimeOffset GetLastWriteTime(SafeFileHandle? handle, ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(handle, path, continueOnError); if (!EntryExists) return new DateTimeOffset(DateTime.FromFileTimeUtc(0)); return UnixTimeToDateTimeOffset(_fileCache.MTime, _fileCache.MTimeNsec); } internal void SetLastWriteTime(string path, DateTimeOffset time, bool asDirectory) => SetLastWriteTime(handle: null, path, time, asDirectory); internal void SetLastWriteTime(SafeFileHandle handle, DateTimeOffset time, bool asDirectory) => SetLastWriteTime(handle, handle.Path, time, asDirectory); internal void SetLastWriteTime(SafeFileHandle? handle, string? path, DateTimeOffset time, bool asDirectory) => SetAccessOrWriteTime(handle, path, time, isAccessTime: false, asDirectory); private static DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds) { return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick); } private unsafe void SetAccessOrWriteTimeCore(SafeFileHandle? handle, string? path, DateTimeOffset time, bool isAccessTime, bool checkCreationTime, bool asDirectory) { // This api is used to set creation time on non OSX platforms, and as a fallback for OSX platforms. // The reason why we use it to set 'creation time' is the below comment: // 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). // force a refresh so that we have an up-to-date times for values not being overwritten InvalidateCaches(); EnsureCachesInitialized(handle, path); if (!EntryExists) ThrowNotFound(path); // we use utimes()/utimensat() to set the accessTime and writeTime Interop.Sys.TimeSpec* buf = stackalloc Interop.Sys.TimeSpec[2]; long seconds = time.ToUnixTimeSeconds(); long nanoseconds = UnixTimeSecondsToNanoseconds(time, seconds); if (isAccessTime) { buf[0].TvSec = seconds; buf[0].TvNsec = nanoseconds; buf[1].TvSec = _fileCache.MTime; buf[1].TvNsec = _fileCache.MTimeNsec; } else { buf[0].TvSec = _fileCache.ATime; buf[0].TvNsec = _fileCache.ATimeNsec; buf[1].TvSec = seconds; buf[1].TvNsec = nanoseconds; } int rv = handle is not null ? Interop.Sys.FUTimens(handle, buf) : Interop.Sys.UTimensat(path!, buf); Interop.CheckIo(rv, path, asDirectory); // On OSX-like platforms, when the modification time is less than the creation time (including // when the modification time is already less than but access time is being set), the creation // time is set to the modification time due to the api we're currently using; this is not // desirable behaviour since it is inconsistent with windows behaviour and is not logical to // the programmer (ie. we'd have to document it), so these api calls revert the creation time // when it shouldn't be set (since we're setting modification time and access time here). // checkCreationTime is only true on OSX-like platforms. // allowFallbackToLastWriteTime is ignored on non OSX-like platforms. bool updateCreationTime = checkCreationTime && (_fileCache.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0 && (buf[1].TvSec < _fileCache.BirthTime || (buf[1].TvSec == _fileCache.BirthTime && buf[1].TvNsec < _fileCache.BirthTimeNsec)); InvalidateCaches(); if (updateCreationTime) { Interop.Error error = SetCreationTimeCore(handle, path, _fileCache.BirthTime, _fileCache.BirthTimeNsec); if (error != Interop.Error.SUCCESS && error != Interop.Error.ENOTSUP) { Interop.CheckIo(error, path, asDirectory); } } } internal long GetLength(ReadOnlySpan<char> path, bool continueOnError = false) { // For symbolic links, on Windows, Length returns zero and not the target file size. // On Unix, it returns the length of the path stored in the link. EnsureCachesInitialized(path, continueOnError); return EntryExists ? _fileCache.Size : 0; } internal UnixFileMode GetUnixFileMode(ReadOnlySpan<char> path, bool continueOnError = false) => GetUnixFileMode(handle: null, path, continueOnError); internal UnixFileMode GetUnixFileMode(SafeFileHandle handle, bool continueOnError = false) => GetUnixFileMode(handle, handle.Path, continueOnError); private UnixFileMode GetUnixFileMode(SafeFileHandle? handle, ReadOnlySpan<char> path, bool continueOnError = false) { EnsureCachesInitialized(handle, path, continueOnError); if (!EntryExists || IsBrokenLink) return (UnixFileMode)(-1); return (UnixFileMode)(_fileCache.Mode & (int)FileSystem.ValidUnixFileModes); } internal void SetUnixFileMode(string path, UnixFileMode mode) => SetUnixFileMode(handle: null, path, mode); internal void SetUnixFileMode(SafeFileHandle handle, UnixFileMode mode) => SetUnixFileMode(handle, handle.Path, mode); private void SetUnixFileMode(SafeFileHandle? handle, string? path, UnixFileMode mode) { if ((mode & ~FileSystem.ValidUnixFileModes) != 0) { throw new ArgumentException(SR.Arg_InvalidUnixFileMode, nameof(UnixFileMode)); } // Linux does not support link permissions. // To have consistent cross-platform behavior we operate on the link target. int rv = handle is not null ? Interop.Sys.FChMod(handle, (int)mode) : Interop.Sys.ChMod(path!, (int)mode); Interop.CheckIo(rv, path); InvalidateCaches(); } internal void RefreshCaches(ReadOnlySpan<char> path) => RefreshCaches(handle: null, path); // 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(SafeFileHandle? handle, ReadOnlySpan<char> path) { Debug.Assert(handle is not null || path.Length > 0); #if !TARGET_BROWSER && !TARGET_WASI _isReadOnlyCache = -1; #endif int rv = handle is not null ? Interop.Sys.FStat(handle, out _fileCache) : Interop.Sys.LStat(Path.TrimEndingDirectorySeparator(path), out _fileCache); if (rv < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); switch (errorInfo.Error) { case Interop.Error.ENOENT: _state = InitializedNotExists; break; case Interop.Error.ENOTDIR: _state = InitializedNotExistsNotADir; break; default: Debug.Assert(errorInfo.RawErrno > 0); // Expect a positive integer _state = errorInfo.RawErrno; // Initialized with error. break; } return; } // Check if the main path is a directory, or a link to a directory. int fileType = _fileCache.Mode & Interop.Sys.FileTypes.S_IFMT; bool isDirectory = fileType == Interop.Sys.FileTypes.S_IFDIR; if (fileType == Interop.Sys.FileTypes.S_IFLNK) { if (Interop.Sys.Stat(path, out Interop.Sys.FileStatus target) == 0) { isDirectory = (target.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; // Make GetUnixFileMode return target permissions. _fileCache.Mode = Interop.Sys.FileTypes.S_IFLNK | (target.Mode & (int)FileSystem.ValidUnixFileModes); } else { _state = InitializedExistsBrokenLink; return; } } _state = isDirectory ? InitializedExistsDir : InitializedExistsFile; } internal void EnsureCachesInitialized(ReadOnlySpan<char> path, bool continueOnError = false) => EnsureCachesInitialized(handle: null, path, continueOnError); // Checks if the file cache is uninitialized and refreshes it's value. // If it failed, and continueOnError is set to true, this method will throw. internal void EnsureCachesInitialized(SafeFileHandle? handle, ReadOnlySpan<char> path, bool continueOnError = false) { if (_state == Uninitialized) { RefreshCaches(handle, path); } if (!continueOnError) { ThrowOnCacheInitializationError(path); } } // Throws if any of the caches has an error number saved in it private void ThrowOnCacheInitializationError(ReadOnlySpan<char> path) { // Lstat should always be initialized by Refresh int errno = _state; if (errno > 0) { InvalidateCaches(); throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path)); } } private static long UnixTimeSecondsToNanoseconds(DateTimeOffset time, long seconds) { return (time.UtcDateTime.Ticks - DateTimeOffset.UnixEpoch.Ticks - seconds * TimeSpan.TicksPerSecond) * NanosecondsPerTick; } private void ThrowNotFound(string? path) { Interop.Error error = _state == InitializedNotExistsNotADir ? Interop.Error.ENOTDIR : Interop.Error.ENOENT; throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(error), path); } } }