diff --git a/src/libraries/Common/src/Interop/OSX/Interop.libobjc.cs b/src/libraries/Common/src/Interop/OSX/Interop.libobjc.cs index 9234767340f8ca..6d0610d877aec7 100644 --- a/src/libraries/Common/src/Interop/OSX/Interop.libobjc.cs +++ b/src/libraries/Common/src/Interop/OSX/Interop.libobjc.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.IO; using System.Runtime.InteropServices; internal static partial class Interop @@ -56,5 +57,52 @@ internal static Version GetOperatingSystemVersion() [DllImport(Libraries.libobjc, EntryPoint = MessageSendStructReturnEntryPoint)] private static extern NSOperatingSystemVersion get_operatingSystemVersion(IntPtr basePtr, IntPtr selector); + + [DllImport(Libraries.libobjc)] + private static extern IntPtr objc_msgSend(IntPtr basePtr, IntPtr selector, double secs); //used for 'initWithTimeIntervalSince1970:' + [DllImport(Libraries.libobjc)] + private static extern IntPtr objc_msgSend(IntPtr basePtr, IntPtr selector, [MarshalAs(UnmanagedType.LPUTF8Str)] string nullTerminatedCString); //used for 'initWithUTF8String:' + [DllImport(Libraries.libobjc)] + private static extern IntPtr objc_msgSend(IntPtr basePtr, IntPtr selector, IntPtr @object, IntPtr key); //used for 'dictionaryWithObject:forKey:' + [DllImport(Libraries.libobjc)] + private static extern byte objc_msgSend(IntPtr basePtr, IntPtr selector, IntPtr attributes, IntPtr path, IntPtr error); //used for 'setAttributes:ofItemAtPath:error:' + + internal static void SetCreationOrModificationTimeOfFileInternal(string path, bool isModificationDate, DateTimeOffset time) + { + var NSDate = objc_getClass("NSDate"); + var NSDictionary = objc_getClass("NSDictionary"); + var NSFileManager = objc_getClass("NSFileManager"); + var NSString = objc_getClass("NSString"); + var alloc = sel_getUid("alloc"); + var initWithUTF8String_ = sel_getUid("initWithUTF8String:"); + var initWithTimeIntervalSince1970_ = sel_getUid("initWithTimeIntervalSince1970:"); + var dictionaryWithObject_forKey_ = sel_getUid("dictionaryWithObject:forKey:"); + var defaultManager = sel_getUid("defaultManager"); + var setAttributes_ofItemAtPath_error_ = sel_getUid("setAttributes:ofItemAtPath:error:"); + var release = sel_getUid("release"); + var NSFileCreationOrModificationDate = objc_msgSend(objc_msgSend(NSString, alloc), initWithUTF8String_, isModificationDate ? "NSFileModificationDate" : "NSFileCreationDate"); + var DefaultNSFileManager = objc_msgSend(NSFileManager, defaultManager); + + var date = objc_msgSend(NSDate, alloc); + date = objc_msgSend(date, initWithTimeIntervalSince1970_, (time - DateTimeOffset.UnixEpoch).TotalSeconds); + var fileAttributes = objc_msgSend(NSDictionary, dictionaryWithObject_forKey_, date, NSFileCreationOrModificationDate); + var native_filePath = objc_msgSend(NSString, alloc); + native_filePath = objc_msgSend(native_filePath, initWithUTF8String_, path); + try + { + if (objc_msgSend(DefaultNSFileManager, setAttributes_ofItemAtPath_error_, fileAttributes, native_filePath, IntPtr.Zero) != 1) + { + //throw an error of some sort - need to change + throw new IOException("Could not set the creation date of the file."); + } + } + finally + { + objc_msgSend(date, release); + objc_msgSend(fileAttributes, release); + objc_msgSend(native_filePath, release); + objc_msgSend(NSFileCreationOrModificationDate, release); + } + } } } diff --git a/src/libraries/System.IO.FileSystem/src/System.IO.FileSystem.csproj b/src/libraries/System.IO.FileSystem/src/System.IO.FileSystem.csproj index df5ddd614c29f2..aeba3f96ee9dc3 100644 --- a/src/libraries/System.IO.FileSystem/src/System.IO.FileSystem.csproj +++ b/src/libraries/System.IO.FileSystem/src/System.IO.FileSystem.csproj @@ -1,9 +1,9 @@ - + System.IO.FileSystem true true - $(NetCoreAppCurrent)-Windows_NT;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser + $(NetCoreAppCurrent)-Windows_NT;$(NetCoreAppCurrent)-Linux;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-FreeBSD enable @@ -172,8 +172,8 @@ - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.OSX.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.OSX.cs new file mode 100644 index 00000000000000..a7bd40d2bb5ff6 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.OSX.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.IO +{ + internal struct FileStatus + { + private const int NanosecondsPerTick = 100; + + // The last cached stat information about the file + private Interop.Sys.FileStatus _fileStatus; + + // -1 if _fileStatus isn't initialized, 0 if _fileStatus was initialized with no + // errors, or the errno error code. + private int _fileStatusInitialized; + + // 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; } + + // Is a directory as of the last refresh + internal bool _isDirectory; + + // Exists as of the last refresh + private bool _exists; + + internal static void Initialize( + ref FileStatus status, + bool isDirectory) + { + status.InitiallyDirectory = isDirectory; + status._fileStatusInitialized = -1; + } + + internal void Invalidate() => _fileStatusInitialized = -1; + + internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + 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; + } + else if (_fileStatus.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; + } + + 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) + { + // IMPORTANT: Attribute logic must match the logic in FileSystemEntry + + EnsureStatInitialized(path); + + if (!_exists) + return (FileAttributes)(-1); + + FileAttributes attributes = default; + + if (IsReadOnly(path)) + attributes |= FileAttributes.ReadOnly; + + if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK) + 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)) + attributes |= FileAttributes.Hidden; + + return attributes != default ? attributes : FileAttributes.Normal; + } + + public void SetAttributes(string path, FileAttributes attributes) + { + // 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); + + 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); + } + } + } + + // 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) + { + // Give write permission to the owner if the owner has read permission + newMode |= (int)Interop.Sys.Permissions.S_IWUSR; + } + + // Change the permissions on the file + if (newMode != _fileStatus.Mode) + { + Interop.CheckIo(Interop.Sys.ChMod(path, newMode), path, InitiallyDirectory); + } + + _fileStatusInitialized = -1; + } + + internal bool GetExists(ReadOnlySpan path) + { + if (_fileStatusInitialized == -1) + Refresh(path); + + return _exists && InitiallyDirectory == _isDirectory; + } + + internal DateTimeOffset GetCreationTime(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + if (!_exists) + return DateTimeOffset.FromFileTime(0); + + if ((_fileStatus.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0) + return UnixTimeToDateTimeOffset(_fileStatus.BirthTime, _fileStatus.BirthTimeNsec); + + // 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); + + return UnixTimeToDateTimeOffset(_fileStatus.CTime, _fileStatus.CTimeNsec); + } + + internal void SetCreationTime(string path, DateTimeOffset time) => Interop.libobjc.SetCreationOrModificationTimeOfFileInternal(path, false, time); + + internal DateTimeOffset GetLastAccessTime(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + if (!_exists) + return DateTimeOffset.FromFileTime(0); + return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec); + } + + internal void SetLastAccessTime(string path, DateTimeOffset time) => SetAccessOrWriteTime(path, time, isAccessTime: true); + + internal DateTimeOffset GetLastWriteTime(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + if (!_exists) + return DateTimeOffset.FromFileTime(0); + return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec); + } + + internal void SetLastWriteTime(string path, DateTimeOffset time) + { + //the creationTime is checked, as when the modification time is set to a value prior to the creationTime, it also sets the creation time to that, see #39132 + var creationTime = GetCreationTime(path); + Interop.libobjc.SetCreationOrModificationTimeOfFileInternal(path, true, time); + if (time < creationTime) Interop.libobjc.SetCreationOrModificationTimeOfFileInternal(path, false, creationTime); + } + + private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds) + { + return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick).ToLocalTime(); + } + + 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); + + // we use utimes()/utimensat() to set the accessTime and writeTime + Interop.Sys.TimeSpec* buf = stackalloc Interop.Sys.TimeSpec[2]; + + long seconds = time.ToUnixTimeSeconds(); + + const long TicksPerMillisecond = 10000; + const long TicksPerSecond = TicksPerMillisecond * 1000; + long nanoseconds = (time.UtcDateTime.Ticks - DateTimeOffset.UnixEpoch.Ticks - seconds * TicksPerSecond) * NanosecondsPerTick; + + if (isAccessTime) + { + buf[0].TvSec = seconds; + buf[0].TvNsec = nanoseconds; + buf[1].TvSec = _fileStatus.MTime; + buf[1].TvNsec = _fileStatus.MTimeNsec; + } + else + { + buf[0].TvSec = _fileStatus.ATime; + buf[0].TvNsec = _fileStatus.ATimeNsec; + buf[1].TvSec = seconds; + buf[1].TvNsec = nanoseconds; + } + + Interop.CheckIo(Interop.Sys.UTimensat(path, buf), path, InitiallyDirectory); + + _fileStatusInitialized = -1; + } + + internal long GetLength(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + return _fileStatus.Size; + } + + 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. + + // 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) + { + 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 + || errorInfo.Error == Interop.Error.ENOTDIR) + { + _fileStatusInitialized = 0; + _exists = false; + } + else + { + _fileStatusInitialized = errorInfo.RawErrno; + } + return; + } + + _exists = true; + + // IMPORTANT: Is directory logic must match the logic in FileSystemEntry + _isDirectory = (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + + // 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) + { + _isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + + _fileStatusInitialized = 0; + } + + internal void EnsureStatInitialized(ReadOnlySpan path, bool continueOnError = false) + { + if (_fileStatusInitialized == -1) + { + Refresh(path); + } + + if (_fileStatusInitialized != 0 && !continueOnError) + { + int errno = _fileStatusInitialized; + _fileStatusInitialized = -1; + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path)); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.OtherUnix.cs similarity index 100% rename from src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs rename to src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.OtherUnix.cs diff --git a/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs b/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs index 0f62c53ad1eddb..8172f8ae9402bc 100644 --- a/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs +++ b/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs @@ -9,7 +9,7 @@ internal static class IOInputs { - public static bool SupportsSettingCreationTime { get { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); } } + public static bool SupportsSettingCreationTime { get { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) | RuntimeInformation.IsOSPlatform(OSPlatform.OSX); } } public static bool SupportsGettingCreationTime { get { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) | RuntimeInformation.IsOSPlatform(OSPlatform.OSX); } } // Max path length (minus trailing \0). Unix values vary system to system; just using really long values here likely to be more than on the average system. diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index e0769d47d16a41..c3a305ed136a74 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -2,7 +2,7 @@ true true - $(NetCoreAppCurrent)-Windows_NT;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser + $(NetCoreAppCurrent)-Windows_NT;$(NetCoreAppCurrent)-Linux;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-FreeBSD