Skip to content

Commit f9d05bd

Browse files
authored
Fix LastWriteTime and LastAccessTime of a symlink on Windows and Unix (#52639)
* Implement most of the fix for #38824 Reverts the changes in the seperate PR a617a01 to fix #38824. Does not re-enable the test because that relies on #49555, will add a seperate commit to whichever is merged last to enable the SettingUpdatesPropertiesOnSymlink test. * Most of the code for PR #52639 to fix #38824 • Deal with merge conflicts • Add FSOPT_NOFOLLOW for macOS and use it in setattrlist • Use AT_SYMLINK_NOFOLLOW with utimensat (note: there doesn't seem to be an equivalent for utimes) • Add SettingUpdatesPropertiesOnSymlink test to test if it actually sets it on the symlink itself (to the best that we can anyway ie. write + read the times) • Specify FILE_FLAG_OPEN_REPARSE_POINT for CreateFile in FileSystem.Windows.cs (is there any other CreateFile calls that need this?) * Remove additional FILE_FLAG_OPEN_REPARSE_POINT I added FILE_FLAG_OPEN_REPARSE_POINT before and it was then added in another PR, this removes the duplicate entry. * Add missing override keywords Add missing override keywords for abstract members in the tests. * Fix access modifiers Fix access modifiers for tests - were meant to be protected rather than public * Add more symlink tests, rearrange files • Move symlink creation api to better spot in non-base files • Add IsDirectory property for some of the new tests • Change abstract symlink api to CreateSymlink that accepts path and target • Create a CreateSymlinkToItem method that creates a symlink to an item that may be relative that uses the new/modified abstract CreateSymlink method • Add SettingUpdatesPropertiesCore to avoid code duplication • Add tests for the following variants: normal/symlink, target exists/doesn't exist, absolute/relative target • Exclude nonexistent symlink target tests on Unix for Directories since they are counted as files * Fix return type of CreateSymlink in File/GetSetTimes.cs * Remove browser from new symlink tests as it doesn't support creation of symlinks Remove browser from new symlink tests as it doesn't support creation of symlinks * Use lutimes, improve code readability, simplify tests • Use lutimes when it's available • Extract dwFlagsAndAttributes to a variable • Use same year for all tests • Checking to delete old symlink is unnecessary, so don't • Replace var with explicit type * Change year in test to 2014 to reduce diff * Rename symlink tests, use 1 core symlink times function, and check that target times don't change Rename symlink tests, use 1 core symlink times function, and check that target times don't change * Inline RunSymlinkTestPart 'function' Inline RunSymlinkTestPart 'function' so that the code can be reordered so the access time test can be valid. * Share CreateSymlinkToItem call in tests and update comment for clarity * Update symlink time tests • Make SettingUpdatesPropertiesOnSymlink a theory • Remove special case for Unix due to #52639 (comment) (will revert if fails) • Rename isRelative to targetIsRelative for clarity * Remove unnecessary Assert.All * Changes to SettingUpdatesPropertiesOnSymlink test • Rename item field to link field • Don't use if one-liner • Use all time functions since only using UTC isn't necessary • Remove the now-defunct IsDirectory property since we aren't checking it anymore * Remove unnecessary fsi.Refresh() • Remove unnecessary fsi.Refresh() since atime is only updated when reading a file * Updates to test and pal_time.c • Remove targetIsRelative cases • Multi-line if statement • Combine HAVE_LUTIMES and #else conditions to allow more code charing * Remove trailing space
1 parent e85de53 commit f9d05bd

File tree

11 files changed

+92
-8
lines changed

11 files changed

+92
-8
lines changed

src/libraries/Common/src/Interop/OSX/Interop.libc.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ internal struct AttrList
2525

2626
[DllImport(Libraries.libc, EntryPoint = "setattrlist", SetLastError = true)]
2727
internal static unsafe extern int setattrlist(string path, AttrList* attrList, void* attrBuf, nint attrBufSize, CULong options);
28+
29+
internal const uint FSOPT_NOFOLLOW = 0x00000001;
2830
}
2931
}

src/libraries/Native/Unix/Common/pal_config.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
#cmakedefine01 HAVE_GETDOMAINNAME
113113
#cmakedefine01 HAVE_UNAME
114114
#cmakedefine01 HAVE_UTIMENSAT
115+
#cmakedefine01 HAVE_LUTIMES
115116
#cmakedefine01 HAVE_FUTIMES
116117
#cmakedefine01 HAVE_FUTIMENS
117118
#cmakedefine01 HAVE_MKSTEMPS

src/libraries/Native/Unix/System.Native/pal_time.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,21 @@ int32_t SystemNative_UTimensat(const char* path, TimeSpec* times)
3333

3434
updatedTimes[1].tv_sec = (time_t)times[1].tv_sec;
3535
updatedTimes[1].tv_nsec = (long)times[1].tv_nsec;
36-
while (CheckInterrupted(result = utimensat(AT_FDCWD, path, updatedTimes, 0)));
36+
while (CheckInterrupted(result = utimensat(AT_FDCWD, path, updatedTimes, AT_SYMLINK_NOFOLLOW)));
3737
#else
3838
struct timeval updatedTimes[2];
3939
updatedTimes[0].tv_sec = (long)times[0].tv_sec;
4040
updatedTimes[0].tv_usec = (int)times[0].tv_nsec / 1000;
4141

4242
updatedTimes[1].tv_sec = (long)times[1].tv_sec;
4343
updatedTimes[1].tv_usec = (int)times[1].tv_nsec / 1000;
44-
while (CheckInterrupted(result = utimes(path, updatedTimes)));
44+
while (CheckInterrupted(result =
45+
#if HAVE_LUTIMES
46+
lutimes
47+
#else
48+
utimes
49+
#endif
50+
(path, updatedTimes)));
4551
#endif
4652

4753
return result;

src/libraries/Native/Unix/configure.cmake

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,11 @@ check_symbol_exists(
683683
sys/stat.h
684684
HAVE_UTIMENSAT)
685685

686+
check_symbol_exists(
687+
lutimes
688+
sys/time.h
689+
HAVE_LUTIMES)
690+
686691
set (PREVIOUS_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
687692
set (CMAKE_REQUIRED_FLAGS "-Werror -Wsign-conversion")
688693

src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetTimes.cs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public abstract class BaseGetSetTimes<T> : FileSystemTest
2424
protected abstract T GetExistingItem();
2525
protected abstract T GetMissingItem();
2626

27+
protected abstract T CreateSymlink(string path, string pathToTarget);
28+
29+
protected T CreateSymlinkToItem(T item)
30+
{
31+
// Creates a Symlink to 'item' (target may or may not exist)
32+
string itemPath = GetItemPath(item);
33+
return CreateSymlink(path: itemPath + ".link", pathToTarget: itemPath);
34+
}
35+
2736
protected abstract string GetItemPath(T item);
2837

2938
public abstract IEnumerable<TimeFunction> TimeFunctions(bool requiresRoundtripping = false);
@@ -43,11 +52,8 @@ public static TimeFunction Create(SetTime setter, GetTime getter, DateTimeKind k
4352
public DateTimeKind Kind => Item3;
4453
}
4554

46-
[Fact]
47-
public void SettingUpdatesProperties()
55+
private void SettingUpdatesPropertiesCore(T item)
4856
{
49-
T item = GetExistingItem();
50-
5157
Assert.All(TimeFunctions(requiresRoundtripping: true), (function) =>
5258
{
5359
// Checking that milliseconds are not dropped after setter.
@@ -71,6 +77,56 @@ public void SettingUpdatesProperties()
7177
});
7278
}
7379

80+
[Fact]
81+
public void SettingUpdatesProperties()
82+
{
83+
T item = GetExistingItem();
84+
SettingUpdatesPropertiesCore(item);
85+
}
86+
87+
[Theory]
88+
[PlatformSpecific(~TestPlatforms.Browser)] // Browser is excluded as it doesn't support symlinks
89+
[InlineData(false)]
90+
[InlineData(true)]
91+
public void SettingUpdatesPropertiesOnSymlink(bool targetExists)
92+
{
93+
// This test is in this class since it needs all of the time functions.
94+
// This test makes sure that the times are set on the symlink itself.
95+
// It is needed as on OSX for example, the default for most APIs is
96+
// to follow the symlink to completion and set the time on that entry
97+
// instead (eg. the setattrlist will do this without the flag set).
98+
// It is also the same case on unix, with the utimensat function.
99+
// It is a theory since we test both the target existing and missing.
100+
101+
T target = targetExists ? GetExistingItem() : GetMissingItem();
102+
103+
// When the target exists, we want to verify that its times don't change.
104+
105+
T link = CreateSymlinkToItem(target);
106+
if (!targetExists)
107+
{
108+
SettingUpdatesPropertiesCore(link);
109+
}
110+
else
111+
{
112+
// Get the target's initial times
113+
IEnumerable<TimeFunction> timeFunctions = TimeFunctions(requiresRoundtripping: true);
114+
DateTime[] initialTimes = timeFunctions.Select((funcs) => funcs.Getter(target)).ToArray();
115+
116+
SettingUpdatesPropertiesCore(link);
117+
118+
// Ensure that we have the latest times.
119+
if (target is FileSystemInfo fsi)
120+
{
121+
fsi.Refresh();
122+
}
123+
124+
// Ensure the target's times haven't changed.
125+
DateTime[] updatedTimes = timeFunctions.Select((funcs) => funcs.Getter(target)).ToArray();
126+
Assert.Equal(initialTimes, updatedTimes);
127+
}
128+
}
129+
74130
[Fact]
75131
[PlatformSpecific(~TestPlatforms.Browser)] // Browser is excluded as there is only 1 effective time store.
76132
public void SettingUpdatesPropertiesAfterAnother()

src/libraries/System.IO.FileSystem/tests/Directory/GetSetTimes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public class Directory_GetSetTimes : StaticGetSetTimes
99
{
1010
protected override string GetExistingItem() => Directory.CreateDirectory(GetTestFilePath()).FullName;
1111

12+
protected override string CreateSymlink(string path, string pathToTarget) => Directory.CreateSymbolicLink(path, pathToTarget).FullName;
13+
1214
public override IEnumerable<TimeFunction> TimeFunctions(bool requiresRoundtripping = false)
1315
{
1416
if (IOInputs.SupportsGettingCreationTime && (!requiresRoundtripping || IOInputs.SupportsSettingCreationTime))

src/libraries/System.IO.FileSystem/tests/DirectoryInfo/GetSetTimes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class DirectoryInfo_GetSetTimes : InfoGetSetTimes<DirectoryInfo>
1111

1212
protected override DirectoryInfo GetMissingItem() => new DirectoryInfo(GetTestFilePath());
1313

14+
protected override DirectoryInfo CreateSymlink(string path, string pathToTarget) => (DirectoryInfo)Directory.CreateSymbolicLink(path, pathToTarget);
15+
1416
protected override string GetItemPath(DirectoryInfo item) => item.FullName;
1517

1618
protected override void InvokeCreate(DirectoryInfo item) => item.Create();

src/libraries/System.IO.FileSystem/tests/File/GetSetTimes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ protected override string GetExistingItem()
2222
return path;
2323
}
2424

25+
protected override string CreateSymlink(string path, string pathToTarget) => File.CreateSymbolicLink(path, pathToTarget).FullName;
26+
2527
[Fact]
2628
[PlatformSpecific(TestPlatforms.Linux)]
2729
public void BirthTimeIsNotNewerThanLowestOfAccessModifiedTimes()

src/libraries/System.IO.FileSystem/tests/FileInfo/GetSetTimes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ protected override FileInfo GetExistingItem()
1717
return new FileInfo(path);
1818
}
1919

20+
protected override FileInfo CreateSymlink(string path, string pathToTarget) => (FileInfo)File.CreateSymbolicLink(path, pathToTarget);
21+
2022
private static bool HasNonZeroNanoseconds(DateTime dt) => dt.Ticks % 10 != 0;
2123

2224
public FileInfo GetNonZeroMilliseconds()

src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.SetTimes.OSX.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private unsafe Interop.Error SetCreationTimeCore(string path, long seconds, long
4747
attrList.commonAttr = Interop.libc.AttrList.ATTR_CMN_CRTIME;
4848

4949
Interop.Error error =
50-
Interop.libc.setattrlist(path, &attrList, &timeSpec, sizeof(Interop.Sys.TimeSpec), default(CULong)) == 0 ?
50+
Interop.libc.setattrlist(path, &attrList, &timeSpec, sizeof(Interop.Sys.TimeSpec), new CULong(Interop.libc.FSOPT_NOFOLLOW)) == 0 ?
5151
Interop.Error.SUCCESS :
5252
Interop.Sys.GetLastErrorInfo().Error;
5353

0 commit comments

Comments
 (0)