diff --git a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs index 36cdea9e1bd9a..dc5602bb0c66c 100644 --- a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs @@ -15,5 +15,6 @@ internal static partial class Libraries internal const string SystemConfigurationLibrary = "/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration"; internal const string AppleCryptoNative = "libSystem.Security.Cryptography.Native.Apple"; internal const string MsQuic = "libmsquic.dylib"; + internal const string libc = "libc"; } } diff --git a/src/libraries/Common/src/Interop/OSX/Interop.libc.cs b/src/libraries/Common/src/Interop/OSX/Interop.libc.cs new file mode 100644 index 0000000000000..d0b377977a1d6 --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.libc.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class libc + { + [StructLayout(LayoutKind.Sequential)] + internal struct AttrList + { + public ushort bitmapCount; + public ushort reserved; + public uint commonAttr; + public uint volAttr; + public uint dirAttr; + public uint fileAttr; + public uint forkAttr; + + public const ushort ATTR_BIT_MAP_COUNT = 5; + public const uint ATTR_CMN_CRTIME = 0x00000200; + } + + [DllImport(Libraries.libc, EntryPoint = "setattrlist", SetLastError = true)] + internal static unsafe extern int setattrlist(string path, AttrList* attrList, void* attrBuf, nint attrBufSize, CULong options); + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetTimes.cs b/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetTimes.cs index a959e317682f4..8df736132ea3f 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetTimes.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetTimes.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Linq; using System.Threading; using Xunit; @@ -70,6 +71,62 @@ public void SettingUpdatesProperties() }); } + [Fact] + [PlatformSpecific(~TestPlatforms.Browser)] // Browser is excluded as there is only 1 effective time store. + public void SettingUpdatesPropertiesAfterAnother() + { + T item = GetExistingItem(); + + // These linq calls make an IEnumerable of pairs of functions that are not identical + // (eg. not (creationtime, creationtime)), includes both orders as seperate entries + // as they it have different behavior in reverse order (of functions), in addition + // to the pairs of functions, there is a reverse bool that allows a test for both + // increasing and decreasing timestamps as to not limit the test unnecessarily. + // Only testing with utc because it would be hard to check if lastwrite utc was the + // same type of method as lastwrite local since their .Getter fields are different. + // This test is required as some apis change more dates than would be desired (eg. + // utimes()/utimensat() set the write and access times, but as a side effect of + // the implementation, it sets creation time too when the write time is less than + // the creation time). There were issues related to the order in which the dates are + // set, so this test should almost fully eliminate any possibilities of that in the + // future by having a proper test for it. Also, it should be noted that the + // combination (A, B, false) is not the same as (B, A, true). + + // The order that these LINQ expression creates is (when all 3 are available): + // [0] = (creation, access, False), [1] = (creation, access, True), [2] = (creation, write, False), + // [3] = (creation, write, True), [4] = (access, creation, False), [5] = (access, creation, True), + // [6] = (access, write, False), [7] = (access, write, True), [8] = (write, creation, False), + // [9] = (write, creation, True), [10] = (write, access, False), [11] = (write, access, True) + // Or, when creation time setting is not available: + // [0] = (access, write, False), [1] = (access, write, True), + // [2] = (write, access, False), [3] = (write, access, True) + + IEnumerable timeFunctionsUtc = TimeFunctions(requiresRoundtripping: true).Where((f) => f.Kind == DateTimeKind.Utc); + bool[] booleanArray = new bool[] { false, true }; + Assert.All(timeFunctionsUtc.SelectMany((x) => timeFunctionsUtc.SelectMany((y) => booleanArray.Select((reverse) => (x, y, reverse)))).Where((fs) => fs.x.Getter != fs.y.Getter), (functions) => + { + TimeFunction function1 = functions.x; + TimeFunction function2 = functions.y; + bool reverse = functions.reverse; + + // Checking that milliseconds are not dropped after setter. + DateTime dt1 = new DateTime(2002, 12, 1, 12, 3, 3, LowTemporalResolution ? 0 : 321, DateTimeKind.Utc); + DateTime dt2 = new DateTime(2001, 12, 1, 12, 3, 3, LowTemporalResolution ? 0 : 321, DateTimeKind.Utc); + DateTime dt3 = new DateTime(2000, 12, 1, 12, 3, 3, LowTemporalResolution ? 0 : 321, DateTimeKind.Utc); + if (reverse) //reverse the order of setting dates + { + (dt1, dt3) = (dt3, dt1); + } + function1.Setter(item, dt1); + function2.Setter(item, dt2); + function1.Setter(item, dt3); + DateTime result1 = function1.Getter(item); + DateTime result2 = function2.Getter(item); + Assert.Equal(dt3, result1); + Assert.Equal(dt2, result2); + }); + } + [Fact] public void CanGetAllTimesAfterCreation() { diff --git a/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs b/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs index d4304b1e391d5..677a027a732ea 100644 --- a/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs +++ b/src/libraries/System.IO.FileSystem/tests/PortedCommon/IOInputs.cs @@ -8,8 +8,8 @@ internal static class IOInputs { - public static bool SupportsSettingCreationTime => OperatingSystem.IsWindows(); - public static bool SupportsGettingCreationTime => OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); + public static bool SupportsSettingCreationTime => PlatformDetection.IsWindows || PlatformDetection.IsOSXLike; + public static bool SupportsGettingCreationTime => PlatformDetection.IsWindows || PlatformDetection.IsOSXLike; // 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. public static readonly int MaxPath = OperatingSystem.IsWindows() ? 259 : 10000; diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 72c1373106a61..454044940efbf 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2151,6 +2151,9 @@ + + + Common\Interop\Unix\System.Native\Interop.GetEUid.cs @@ -2198,6 +2201,10 @@ Common\Interop\OSX\Interop.Libraries.cs + + Common\Interop\OSX\Interop.libc.cs + + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OSX.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OSX.cs new file mode 100644 index 0000000000000..db485033649ca --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OSX.cs @@ -0,0 +1,60 @@ +// 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 partial struct FileStatus + { + internal void SetCreationTime(string path, DateTimeOffset time) + { + // Try to set the attribute on the file system entry using setattrlist, + // if we get ENOTSUP then it means that "The volume does not support + // setattrlist()", so we fall back to the method used on other unix + // platforms, otherwise we throw an error if we get one, or invalidate + // the cache if successful because otherwise it has invalid information. + // Note: the unix fallback implementation doesn't have a test as we are + // yet to determine which volume types it can fail on, so modify with + // great care. + long seconds = time.ToUnixTimeSeconds(); + long nanoseconds = UnixTimeSecondsToNanoseconds(time, seconds); + Interop.Error error = SetCreationTimeCore(path, seconds, nanoseconds); + + if (error == Interop.Error.SUCCESS) + { + InvalidateCaches(); + } + else if (error == Interop.Error.ENOTSUP) + { + SetAccessOrWriteTimeCore(path, time, isAccessTime: false, checkCreationTime: false); + } + else + { + Interop.CheckIo(error, path, InitiallyDirectory); + } + } + + private unsafe Interop.Error SetCreationTimeCore(string path, long seconds, long nanoseconds) + { + Interop.Sys.TimeSpec timeSpec = default; + + timeSpec.TvSec = seconds; + timeSpec.TvNsec = nanoseconds; + + Interop.libc.AttrList attrList = default; + attrList.bitmapCount = Interop.libc.AttrList.ATTR_BIT_MAP_COUNT; + attrList.commonAttr = Interop.libc.AttrList.ATTR_CMN_CRTIME; + + Interop.Error error = + Interop.libc.setattrlist(path, &attrList, &timeSpec, sizeof(Interop.Sys.TimeSpec), default(CULong)) == 0 ? + Interop.Error.SUCCESS : + Interop.Sys.GetLastErrorInfo().Error; + + return error; + } + + private void SetAccessOrWriteTime(string path, DateTimeOffset time, bool isAccessTime) => + SetAccessOrWriteTimeCore(path, time, isAccessTime, checkCreationTime: true); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OtherUnix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OtherUnix.cs new file mode 100644 index 0000000000000..d9d9b1eeb7c92 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OtherUnix.cs @@ -0,0 +1,20 @@ +// 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 partial struct FileStatus + { + internal void SetCreationTime(string path, DateTimeOffset time) => + SetLastWriteTime(path, time); + + private void SetAccessOrWriteTime(string path, DateTimeOffset time, bool isAccessTime) => + SetAccessOrWriteTimeCore(path, time, isAccessTime, checkCreationTime: false); + + // This is not used on these platforms, but is needed for source compat + private Interop.Error SetCreationTimeCore(string path, long seconds, long nanoseconds) => + throw new InvalidOperationException(); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs index ff4d8a86cb18c..3171852eba1a6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs @@ -6,7 +6,7 @@ namespace System.IO { - internal struct FileStatus + internal partial struct FileStatus { private const int NanosecondsPerTick = 100; @@ -245,20 +245,6 @@ internal DateTimeOffset GetCreationTime(ReadOnlySpan path, bool continueOn return UnixTimeToDateTimeOffset(_fileCache.CTime, _fileCache.CTimeNsec); } - 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 DateTimeOffset GetLastAccessTime(ReadOnlySpan path, bool continueOnError = false) { EnsureCachesInitialized(path, continueOnError); @@ -284,20 +270,29 @@ private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds) return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick); } - private unsafe void SetAccessOrWriteTime(string path, DateTimeOffset time, bool isAccessTime) + private unsafe void SetAccessOrWriteTimeCore(string path, DateTimeOffset time, bool isAccessTime, bool checkCreationTime) { + // 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 - _initializedFileCache = -1; + InvalidateCaches(); EnsureCachesInitialized(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; + long nanoseconds = UnixTimeSecondsToNanoseconds(time, seconds); #if TARGET_BROWSER buf[0].TvSec = seconds; @@ -321,7 +316,28 @@ private unsafe void SetAccessOrWriteTime(string path, DateTimeOffset time, bool } #endif Interop.CheckIo(Interop.Sys.UTimensat(path, buf), path, InitiallyDirectory); - _initializedFileCache = -1; + + // 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(path, _fileCache.BirthTime, _fileCache.BirthTimeNsec); + if (error != Interop.Error.SUCCESS && error != Interop.Error.ENOTSUP) + { + Interop.CheckIo(error, path, InitiallyDirectory); + } + } } internal long GetLength(ReadOnlySpan path, bool continueOnError = false) @@ -399,6 +415,13 @@ private void ThrowOnCacheInitializationError(ReadOnlySpan path) } } + private static long UnixTimeSecondsToNanoseconds(DateTimeOffset time, long seconds) + { + const long TicksPerMillisecond = 10000; + const long TicksPerSecond = TicksPerMillisecond * 1000; + return (time.UtcDateTime.Ticks - DateTimeOffset.UnixEpoch.Ticks - seconds * TicksPerSecond) * NanosecondsPerTick; + } + private bool TryRefreshFileCache(ReadOnlySpan path) => VerifyStatCall(Interop.Sys.LStat(path, out _fileCache), out _initializedFileCache);