Skip to content

Commit b37f018

Browse files
davidkayastephentoub
authored andcommitted
Added support for UF_HIDDEN flag on macOS and FreeBSD (#29323) (dotnet#34560)
* Added support for UF_HIDDEN flag on macOS and FreeBSD (#29323) * Changes based on review * Reverted UserFlags field * Sorted include in csproj * Changed comparison style * Reverted UserFlags field * Fixed parentheses * Changes based on PR review * Add missing newline * Add missing newline
1 parent ea48dae commit b37f018

File tree

9 files changed

+161
-11
lines changed

9 files changed

+161
-11
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Runtime.InteropServices;
7+
8+
internal static partial class Interop
9+
{
10+
internal static partial class Sys
11+
{
12+
[Flags]
13+
internal enum UserFlags : uint
14+
{
15+
UF_HIDDEN = 0x8000
16+
}
17+
18+
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_LChflags", SetLastError = true)]
19+
internal static extern int LChflags(string path, uint flags);
20+
21+
internal static readonly bool CanSetHiddenFlag = (LChflagsCanSetHiddenFlag() != 0);
22+
23+
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_LChflagsCanSetHiddenFlag")]
24+
private static extern int LChflagsCanSetHiddenFlag();
25+
}
26+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#cmakedefine01 HAVE_STAT_TIMESPEC
1818
#cmakedefine01 HAVE_STAT_TIM
1919
#cmakedefine01 HAVE_STAT_NSEC
20+
#cmakedefine01 HAVE_STAT_FLAGS
21+
#cmakedefine01 HAVE_LCHFLAGS
2022
#cmakedefine01 HAVE_GNU_STRERROR_R
2123
#cmakedefine01 HAVE_READDIR_R
2224
#cmakedefine01 HAVE_DIRENT_NAME_LEN

src/Native/Unix/System.Native/pal_io.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ static void ConvertFileStatus(const struct stat_* src, FileStatus* dst)
166166
dst->BirthTime = 0;
167167
dst->BirthTimeNsec = 0;
168168
#endif
169+
170+
#if defined(HAVE_STAT_FLAGS) && defined(UF_HIDDEN)
171+
dst->UserFlags = ((src->st_flags & UF_HIDDEN) == UF_HIDDEN) ? PAL_UF_HIDDEN : 0;
172+
#else
173+
dst->UserFlags = 0;
174+
#endif
169175
}
170176

171177
int32_t SystemNative_Stat(const char* path, FileStatus* output)
@@ -1386,3 +1392,25 @@ int32_t SystemNative_LockFileRegion(intptr_t fd, int64_t offset, int64_t length,
13861392
while ((ret = fcntl (ToFileDescriptor(fd), F_SETLK, &lockArgs)) < 0 && errno == EINTR);
13871393
return ret;
13881394
}
1395+
1396+
int32_t SystemNative_LChflags(const char* path, uint32_t flags)
1397+
{
1398+
#if HAVE_LCHFLAGS
1399+
int32_t result;
1400+
while ((result = lchflags(path, flags)) < 0 && errno == EINTR);
1401+
return result;
1402+
#else
1403+
(void)path, (void)flags;
1404+
errno = ENOTSUP;
1405+
return -1;
1406+
#endif
1407+
}
1408+
1409+
int32_t SystemNative_LChflagsCanSetHiddenFlag(void)
1410+
{
1411+
#if defined(UF_HIDDEN) && defined(HAVE_STAT_FLAGS) && defined(HAVE_LCHFLAGS)
1412+
return true;
1413+
#else
1414+
return false;
1415+
#endif
1416+
}

src/Native/Unix/System.Native/pal_io.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ enum
151151
FILESTATUS_FLAGS_HAS_BIRTHTIME = 1,
152152
};
153153

154+
/**
155+
* Constants for interpreting FileStatus.UserFlags.
156+
*/
157+
enum
158+
{
159+
PAL_UF_HIDDEN = 0x8000
160+
};
161+
154162
/**
155163
* Constants from dirent.h for the inode type returned from readdir variants
156164
*/
@@ -705,3 +713,17 @@ DLLEXPORT int32_t SystemNative_GetPeerID(intptr_t socket, uid_t* euid);
705713
* Returns 0 on success, or -1 if an error occurred (in which case, errno is set appropriately).
706714
*/
707715
DLLEXPORT int32_t SystemNative_LockFileRegion(intptr_t fd, int64_t offset, int64_t length, int16_t lockType);
716+
717+
/**
718+
* Changes the file flags of the file whose location is specified in path
719+
*
720+
* Returns 0 for success, -1 for failure. Sets errno for failure.
721+
*/
722+
DLLEXPORT int32_t SystemNative_LChflags(const char* path, uint32_t flags);
723+
724+
/**
725+
* Determines if the current platform supports setting UF_HIDDEN (0x8000) flag
726+
*
727+
* Returns true (non-zero) if supported, false (zero) if not.
728+
*/
729+
DLLEXPORT int32_t SystemNative_LChflagsCanSetHiddenFlag(void);

src/Native/Unix/configure.cmake

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ check_struct_has_member(
205205
"sys/types.h;sys/stat.h"
206206
HAVE_STAT_BIRTHTIME)
207207

208+
check_struct_has_member(
209+
"struct stat"
210+
st_flags
211+
"sys/types.h;sys/stat.h"
212+
HAVE_STAT_FLAGS)
213+
214+
check_symbol_exists(
215+
lchflags
216+
"sys/types.h;sys/stat.h"
217+
HAVE_LCHFLAGS)
218+
208219
check_struct_has_member(
209220
"struct stat"
210221
st_atimespec

src/System.IO.FileSystem/src/System.IO.FileSystem.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,9 @@
279279
<Compile Include="$(CommonPath)\CoreLib\Interop\Unix\System.Native\Interop.Read.cs">
280280
<Link>Common\Interop\Unix\Interop.Read.cs</Link>
281281
</Compile>
282+
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.LChflags.cs">
283+
<Link>Common\Interop\Unix\Interop.LChflags.cs</Link>
284+
</Compile>
282285
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Rename.cs">
283286
<Link>Common\Interop\Unix\Interop.Rename.cs</Link>
284287
</Compile>

src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char>
8484
if (_isDirectory)
8585
attributes |= FileAttributes.Directory;
8686

87-
// If the filename starts with a period, it's hidden.
88-
if (fileName.Length > 0 && fileName[0] == '.')
87+
// If the filename starts with a period or has UF_HIDDEN flag set, it's hidden.
88+
if (fileName.Length > 0 && (fileName[0] == '.' || (_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN))
8989
attributes |= FileAttributes.Hidden;
9090

9191
return attributes != default ? attributes : FileAttributes.Normal;
@@ -98,10 +98,10 @@ public void SetAttributes(string path, FileAttributes attributes)
9898
const FileAttributes allValidFlags =
9999
FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Device |
100100
FileAttributes.Directory | FileAttributes.Encrypted | FileAttributes.Hidden |
101-
FileAttributes.Hidden | FileAttributes.IntegrityStream | FileAttributes.Normal |
102-
FileAttributes.NoScrubData | FileAttributes.NotContentIndexed | FileAttributes.Offline |
103-
FileAttributes.ReadOnly | FileAttributes.ReparsePoint | FileAttributes.SparseFile |
104-
FileAttributes.System | FileAttributes.Temporary;
101+
FileAttributes.IntegrityStream | FileAttributes.Normal | FileAttributes.NoScrubData |
102+
FileAttributes.NotContentIndexed | FileAttributes.Offline | FileAttributes.ReadOnly |
103+
FileAttributes.ReparsePoint | FileAttributes.SparseFile | FileAttributes.System |
104+
FileAttributes.Temporary;
105105
if ((attributes & ~allValidFlags) != 0)
106106
{
107107
// Using constant string for argument to match historical throw
@@ -113,6 +113,26 @@ public void SetAttributes(string path, FileAttributes attributes)
113113
if (!_exists)
114114
FileSystemInfo.ThrowNotFound(path);
115115

116+
if (Interop.Sys.CanSetHiddenFlag)
117+
{
118+
if ((attributes & FileAttributes.Hidden) != 0)
119+
{
120+
if ((_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == 0)
121+
{
122+
// If Hidden flag is set and cached file status does not have the flag set then set it
123+
Interop.CheckIo(Interop.Sys.LChflags(path, (_fileStatus.UserFlags | (uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory);
124+
}
125+
}
126+
else
127+
{
128+
if ((_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN)
129+
{
130+
// If Hidden flag is not set and cached file status does have the flag set then remove it
131+
Interop.CheckIo(Interop.Sys.LChflags(path, (_fileStatus.UserFlags & ~(uint)Interop.Sys.UserFlags.UF_HIDDEN)), path, InitiallyDirectory);
132+
}
133+
}
134+
}
135+
116136
// The only thing we can reasonably change is whether the file object is readonly by changing permissions.
117137

118138
int newMode = _fileStatus.Mode;

src/System.IO.FileSystem/tests/Base/FileGetSetAttributes.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ public abstract class FileGetSetAttributes : BaseGetSetAttributes
1616
public void SettingAttributes_Unix(FileAttributes attributes)
1717
{
1818
string path = CreateItem();
19-
SetAttributes(path, attributes);
20-
Assert.Equal(attributes, GetAttributes(path));
21-
SetAttributes(path, 0);
19+
AssertSettingAttributes(path, attributes);
20+
}
21+
22+
[Theory]
23+
[InlineData(FileAttributes.Hidden)]
24+
[PlatformSpecific(TestPlatforms.OSX | TestPlatforms.FreeBSD)]
25+
public void SettingAttributes_OSXAndFreeBSD(FileAttributes attributes)
26+
{
27+
string path = CreateItem();
28+
AssertSettingAttributes(path, attributes);
2229
}
2330

2431
[Theory]
@@ -33,6 +40,11 @@ public void SettingAttributes_Unix(FileAttributes attributes)
3340
public void SettingAttributes_Windows(FileAttributes attributes)
3441
{
3542
string path = CreateItem();
43+
AssertSettingAttributes(path, attributes);
44+
}
45+
46+
private void AssertSettingAttributes(string path, FileAttributes attributes)
47+
{
3648
SetAttributes(path, attributes);
3749
Assert.Equal(attributes, GetAttributes(path));
3850
SetAttributes(path, 0);
@@ -48,8 +60,16 @@ public void SettingAttributes_Windows(FileAttributes attributes)
4860
public void SettingInvalidAttributes_Unix(FileAttributes attributes)
4961
{
5062
string path = CreateItem();
51-
SetAttributes(path, attributes);
52-
Assert.Equal(FileAttributes.Normal, GetAttributes(path));
63+
AssertSettingInvalidAttributes(path, attributes);
64+
}
65+
66+
[Theory]
67+
[InlineData(FileAttributes.Hidden)]
68+
[PlatformSpecific(TestPlatforms.AnyUnix & ~(TestPlatforms.OSX | TestPlatforms.FreeBSD))]
69+
public void SettingInvalidAttributes_UnixExceptOSXAndFreeBSD(FileAttributes attributes)
70+
{
71+
string path = CreateItem();
72+
AssertSettingInvalidAttributes(path, attributes);
5373
}
5474

5575
[Theory]
@@ -62,6 +82,11 @@ public void SettingInvalidAttributes_Unix(FileAttributes attributes)
6282
public void SettingInvalidAttributes_Windows(FileAttributes attributes)
6383
{
6484
string path = CreateItem();
85+
AssertSettingInvalidAttributes(path, attributes);
86+
}
87+
88+
private void AssertSettingInvalidAttributes(string path, FileAttributes attributes)
89+
{
6590
SetAttributes(path, attributes);
6691
Assert.Equal(FileAttributes.Normal, GetAttributes(path));
6792
}

src/System.IO.FileSystem/tests/FileInfo/GetSetAttributes.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,18 @@ public void IsReadOnly_SetAndGet()
2828
test.Refresh();
2929
Assert.Equal(false, test.IsReadOnly);
3030
}
31+
32+
[Theory]
33+
[InlineData(".", true)]
34+
[InlineData("", false)]
35+
[PlatformSpecific(TestPlatforms.OSX)]
36+
public void HiddenAttributeSetCorrectly_OSX(string filePrefix, bool hidden)
37+
{
38+
string testFilePath = Path.Combine(TestDirectory, $"{filePrefix}{GetTestFileName()}");
39+
FileInfo fileInfo = new FileInfo(testFilePath);
40+
fileInfo.Create().Dispose();
41+
42+
Assert.Equal(hidden, (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden);
43+
}
3144
}
3245
}

0 commit comments

Comments
 (0)