Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: FileSystemEntry.Attributes property is not correct on Unix #52235

Merged
merged 17 commits into from
May 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public unsafe ref partial struct FileSystemEntry
private ReadOnlySpan<char> _fullPath;
private ReadOnlySpan<char> _fileName;
private fixed char _fileNameBuffer[Interop.Sys.DirectoryEntry.NameBufferSize];
private FileAttributes _initialAttributes;

internal static FileAttributes Initialize(
ref FileSystemEntry entry,
Expand All @@ -34,52 +33,37 @@ internal static FileAttributes Initialize(
entry._fullPath = ReadOnlySpan<char>.Empty;
entry._fileName = ReadOnlySpan<char>.Empty;

// IMPORTANT: Attribute logic must match the logic in FileStatus
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved
entry._status.InvalidateCaches();

bool isDirectory = directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR;
bool isSymlink = directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK;
bool isUnknown = directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN;

bool isDirectory = false;
bool isSymlink = false;
if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR)
{
// We know it's a directory.
isDirectory = true;
}
// Some operating systems don't have the inode type in the dirent structure,
// so we use DT_UNKNOWN as a sentinel value. As such, check if the dirent is a
// directory.
else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK
|| directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
&& Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus targetStatus) >= 0)
// symlink or a directory.
if (isUnknown)
{
// Symlink or unknown: Stat to it to see if we can resolve it to a directory.
isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
isSymlink = entry.IsSymbolicLink;
isDirectory = entry._status.IsDirectory(entry.FullPath);
}
// Same idea as the directory check, just repeated for (and tweaked due to the
// nature of) symlinks.
if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK)
{
isSymlink = true;
}
else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
&& (Interop.Sys.LStat(entry.FullPath, out Interop.Sys.FileStatus linkTargetStatus) >= 0))
// Whether we had the dirent structure or not, we treat a symlink to a directory as a directory,
// so we need to reflect that in our isDirectory variable.
else if (isSymlink)
{
isSymlink = (linkTargetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK;
isDirectory = entry._status.IsDirectory(entry.FullPath);
}

entry._status = default;
FileStatus.Initialize(ref entry._status, isDirectory);
entry._status.InitiallyDirectory = isDirectory;

FileAttributes attributes = default;
if (isSymlink)
attributes |= FileAttributes.ReparsePoint;
if (isDirectory)
attributes |= FileAttributes.Directory;
if (directoryEntry.Name[0] == '.')
attributes |= FileAttributes.Hidden;

if (attributes == default)
attributes = FileAttributes.Normal;
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved

entry._initialAttributes = attributes;
return attributes;
}

Expand Down Expand Up @@ -133,17 +117,15 @@ public ReadOnlySpan<char> FileName

// Windows never fails getting attributes, length, or time as that information comes back
// with the native enumeration struct. As such we must not throw here.

public FileAttributes Attributes
// It would be hard to rationalize if the attributes change after our initial find.
=> _initialAttributes | (_status.IsReadOnly(FullPath, continueOnError: true) ? FileAttributes.ReadOnly : 0);

public FileAttributes Attributes => _status.GetAttributes(FullPath, FileName);
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved
public long Length => _status.GetLength(FullPath, continueOnError: true);
public DateTimeOffset CreationTimeUtc => _status.GetCreationTime(FullPath, continueOnError: true);
public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true);
public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true);
public bool IsDirectory => _status.InitiallyDirectory;
public bool IsHidden => _directoryEntry.Name[0] == '.';
public bool IsHidden => _status.IsHidden(FullPath, FileName);
internal bool IsReadOnly => _status.IsReadOnly(FullPath);
internal bool IsSymbolicLink => _status.IsSymbolicLink(FullPath);

public FileSystemInfo ToFileSystemInfo()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public bool MoveNext()
FileAttributes attributes = FileSystemEntry.Initialize(
ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer));
bool isDirectory = (attributes & FileAttributes.Directory) != 0;
bool isSymlink = (attributes & FileAttributes.ReparsePoint) != 0;

bool isSpecialDirectory = false;
if (isDirectory)
Expand All @@ -130,13 +131,12 @@ public bool MoveNext()

if (!isSpecialDirectory && _options.AttributesToSkip != 0)
{
if ((_options.AttributesToSkip & FileAttributes.ReadOnly) != 0)
{
// ReadOnly is the only attribute that requires hitting entry.Attributes (which hits the disk)
attributes = entry.Attributes;
}

if ((_options.AttributesToSkip & attributes) != 0)
// entry.IsHidden and entry.IsReadOnly will hit the disk if the caches had not been
// initialized yet and we could not soft-retrieve the attributes in Initialize
if ((ShouldSkip(FileAttributes.Directory) && isDirectory) ||
(ShouldSkip(FileAttributes.ReparsePoint) && isSymlink) ||
(ShouldSkip(FileAttributes.Hidden) && entry.IsHidden) ||
(ShouldSkip(FileAttributes.ReadOnly) && entry.IsReadOnly))
{
continue;
}
Expand All @@ -161,6 +161,8 @@ public bool MoveNext()
} while (true);
}
}

bool ShouldSkip(FileAttributes attributeToSkip) => (_options.AttributesToSkip & attributeToSkip) != 0;
}

private unsafe void FindNextEntry()
Expand Down
Loading