Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions Files/Filesystem/FilesystemOperations/FilesystemOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ await DialogDisplayHelper.ShowDialogAsync(

if (fsCopyResult)
{
if (FolderHelpers.CheckFolderForHiddenAttribute(source.Path))
if (NativeFileOperationsHelper.HasFileAttribute(source.Path, FileAttributes.Hidden))
{
// The source folder was hidden, apply hidden attribute to destination
NativeFileOperationsHelper.SetFileAttribute(fsCopyResult.Result.Path, FileAttributes.Hidden);
Expand Down Expand Up @@ -469,7 +469,7 @@ await DialogDisplayHelper.ShowDialogAsync(

if (fsResultMove)
{
if (FolderHelpers.CheckFolderForHiddenAttribute(source.Path))
if (NativeFileOperationsHelper.HasFileAttribute(source.Path, FileAttributes.Hidden))
{
// The source folder was hidden, apply hidden attribute to destination
NativeFileOperationsHelper.SetFileAttribute(fsResultMove.Result.Path, FileAttributes.Hidden);
Expand Down
20 changes: 0 additions & 20 deletions Files/Filesystem/FolderHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.IO;
using System.Threading.Tasks;
using static Files.Helpers.NativeFindStorageItemHelper;
using FileAttributes = System.IO.FileAttributes;

namespace Files.Filesystem
{
Expand All @@ -24,25 +23,6 @@ public static bool CheckFolderAccessWithWin32(string path)
return false;
}

public static bool CheckFolderForHiddenAttribute(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
int additionalFlags = FIND_FIRST_EX_LARGE_FETCH;
IntPtr hFileTsk = FindFirstFileExFromApp(path + "\\*.*", findInfoLevel, out WIN32_FIND_DATA findDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero,
additionalFlags);
if (hFileTsk.ToInt64() == -1)
{
return false;
}
var isHidden = ((FileAttributes)findDataTsk.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
FindClose(hFileTsk);
return isHidden;
}

public static async Task<bool> CheckBitlockerStatusAsync(BaseStorageFolder rootFolder, string path)
{
if (rootFolder == null || rootFolder.Properties == null)
Expand Down
1 change: 1 addition & 0 deletions Files/Filesystem/ListedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ public ShortcutItem() : base()
public string WorkingDirectory { get; set; }
public bool RunAsAdmin { get; set; }
public bool IsUrl { get; set; }
public bool IsSymLink { get; set; }
public override bool IsExecutable => Path.GetExtension(TargetPath)?.ToLower() == ".exe";
}

Expand Down
51 changes: 33 additions & 18 deletions Files/Filesystem/StorageEnumerators/Win32StorageEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,38 @@ CancellationToken cancellationToken
return null;
}

if (findData.cFileName.EndsWith(".lnk") || findData.cFileName.EndsWith(".url"))
bool isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
double opacity = isHidden ? Constants.UI.DimItemOpacity : 1;

// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4
bool isReparsePoint = ((FileAttributes)findData.dwFileAttributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
bool isSymlink = isReparsePoint && findData.dwReserved0 == NativeFileOperationsHelper.IO_REPARSE_TAG_SYMLINK;

if (isSymlink)
{
var targetPath = NativeFileOperationsHelper.ParseSymLink(itemPath);
return new ShortcutItem(null, dateReturnFormat)
{
PrimaryItemAttribute = StorageItemTypes.File,
FileExtension = itemFileExtension,
IsHiddenItem = isHidden,
Opacity = opacity,
FileImage = null,
LoadFileIcon = itemThumbnailImgVis,
LoadWebShortcutGlyph = false,
ItemName = itemName,
ItemDateModifiedReal = itemModifiedDate,
ItemDateAccessedReal = itemLastAccessDate,
ItemDateCreatedReal = itemCreatedDate,
ItemType = "ShortcutFileType".GetLocalized(),
ItemPath = itemPath,
FileSize = itemSize,
FileSizeBytes = itemSizeBytes,
TargetPath = targetPath,
IsSymLink = true
};
}
else if (findData.cFileName.EndsWith(".lnk") || findData.cFileName.EndsWith(".url"))
{
if (connection != null)
{
Expand All @@ -267,14 +298,6 @@ CancellationToken cancellationToken
var isUrl = findData.cFileName.EndsWith(".url");
string target = (string)response["TargetPath"];

bool isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden);
double opacity = 1;

if (isHidden)
{
opacity = Constants.UI.DimItemOpacity;
}

return new ShortcutItem(null, dateReturnFormat)
{
PrimaryItemAttribute = (bool)response["IsFolder"] ? StorageItemTypes.Folder : StorageItemTypes.File,
Expand Down Expand Up @@ -311,14 +334,6 @@ CancellationToken cancellationToken
}
else
{
bool isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden);
double opacity = 1;

if (isHidden)
{
opacity = Constants.UI.DimItemOpacity;
}

if (".zip".Equals(itemFileExtension, StringComparison.OrdinalIgnoreCase) && await ZipStorageFolder.CheckDefaultZipApp(itemPath))
{
return new ZipItem(null, dateReturnFormat)
Expand Down Expand Up @@ -363,4 +378,4 @@ CancellationToken cancellationToken
return null;
}
}
}
}
69 changes: 67 additions & 2 deletions Files/Helpers/NativeFileOperationsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,44 @@ public static SafeFileHandle CreateFileForWrite(string filePath, bool overwrite
GENERIC_WRITE, 0, IntPtr.Zero, overwrite ? CREATE_ALWAYS : OPEN_ALWAYS, (uint)File_Attributes.BackupSemantics, IntPtr.Zero), true);
}

public static SafeFileHandle OpenFileForRead(string filePath, bool readWrite = false)
public static SafeFileHandle OpenFileForRead(string filePath, bool readWrite = false, uint flags = 0)
{
return new SafeFileHandle(CreateFileFromApp(filePath,
GENERIC_READ | (readWrite ? GENERIC_WRITE : 0), FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_ALWAYS, (uint)File_Attributes.BackupSemantics, IntPtr.Zero), true);
GENERIC_READ | (readWrite ? GENERIC_WRITE : 0), FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_ALWAYS, (uint)File_Attributes.BackupSemantics | flags, IntPtr.Zero), true);
}

private const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
public const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
public const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct REPARSE_DATA_BUFFER
{
public uint ReparseTag;
public short ReparseDataLength;
public short Reserved;
public short SubsNameOffset;
public short SubsNameLength;
public short PrintNameOffset;
public short PrintNameLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = MAXIMUM_REPARSE_DATA_BUFFER_SIZE)]
public char[] PathBuffer;
}

[DllImport("api-ms-win-core-io-l1-1-0.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
IntPtr hDevice,
uint dwIoControlCode,
IntPtr lpInBuffer,
uint nInBufferSize,
//IntPtr lpOutBuffer,
out REPARSE_DATA_BUFFER outBuffer,
uint nOutBufferSize,
out uint lpBytesReturned,
IntPtr lpOverlapped);

[DllImport("api-ms-win-core-file-fromapp-l1-1-0.dll", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
Expand Down Expand Up @@ -357,5 +389,38 @@ public static async Task<SafeFileHandle> OpenProtectedFileForRead(string filePat
}
return new SafeFileHandle(new IntPtr(-1), true);
}

// https://github.com/rad1oactive/BetterExplorer/blob/master/Windows%20API%20Code%20Pack%201.1/source/WindowsAPICodePack/Shell/ReparsePoint.cs
public static string ParseSymLink(string path)
{
using var handle = OpenFileForRead(path, false, 0x00200000);
if (!handle.IsInvalid)
{
REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
if (DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0, out buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, out _, IntPtr.Zero))
{
var subsString = new string(buffer.PathBuffer, ((buffer.SubsNameOffset / 2) + 2), buffer.SubsNameLength / 2);
var printString = new string(buffer.PathBuffer, ((buffer.PrintNameOffset / 2) + 2), buffer.PrintNameLength / 2);
var normalisedTarget = printString ?? subsString;
if (string.IsNullOrEmpty(normalisedTarget))
{
normalisedTarget = subsString;
if (normalisedTarget.StartsWith(@"\??\"))
{
normalisedTarget = normalisedTarget.Substring(4);
}
}
if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK && (normalisedTarget.Length < 2 || normalisedTarget[1] != ':'))
{
// Target is relative, get the absolute path
normalisedTarget = normalisedTarget.TrimStart(Path.DirectorySeparatorChar);
path = path.TrimEnd(Path.DirectorySeparatorChar);
normalisedTarget = Path.GetFullPath(Path.Combine(path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)), normalisedTarget));
}
return normalisedTarget;
}
}
return null;
}
}
}
14 changes: 14 additions & 0 deletions Files/Helpers/NativeFindStorageItemHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,19 @@ public static extern IntPtr FindFirstFileExFromApp(

[DllImport("api-ms-win-core-timezone-l1-1-0.dll", SetLastError = true)]
public static extern bool FileTimeToSystemTime(ref FILETIME lpFileTime, out SYSTEMTIME lpSystemTime);

public static bool GetWin32FindDataForPath(string targetPath, out WIN32_FIND_DATA findData)
{
FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
int additionalFlags = FIND_FIRST_EX_LARGE_FETCH;

IntPtr hFile = FindFirstFileExFromApp(targetPath, findInfoLevel, out findData, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, additionalFlags);
if (hFile.ToInt64() != -1)
{
FindClose(hFile);
return true;
}
return false;
}
}
}
22 changes: 19 additions & 3 deletions Files/Helpers/NavigationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ public static async Task<bool> OpenPath(string path, IShellPage associatedInstan
{
string previousDir = associatedInstance.FilesystemViewModel.WorkingDirectory;
bool isHiddenItem = NativeFileOperationsHelper.HasFileAttribute(path, System.IO.FileAttributes.Hidden);
bool isShortcutItem = path.EndsWith(".lnk") || path.EndsWith(".url"); // Determine
bool isDirectory = NativeFileOperationsHelper.HasFileAttribute(path, System.IO.FileAttributes.Directory);
bool isReparsePoint = NativeFileOperationsHelper.HasFileAttribute(path, System.IO.FileAttributes.ReparsePoint);
bool isShortcutItem = path.EndsWith(".lnk") || path.EndsWith(".url");
FilesystemResult opened = (FilesystemResult)false;

var shortcutInfo = new ShortcutItem();
if (itemType == null || isShortcutItem || isHiddenItem)
if (itemType == null || isShortcutItem || isHiddenItem || isReparsePoint)
{
if (isShortcutItem)
{
Expand Down Expand Up @@ -159,6 +161,20 @@ public static async Task<bool> OpenPath(string path, IShellPage associatedInstan
return false;
}
}
else if (isReparsePoint)
{
if (!isDirectory)
{
if (NativeFindStorageItemHelper.GetWin32FindDataForPath(path, out var findData))
{
if (findData.dwReserved0 == NativeFileOperationsHelper.IO_REPARSE_TAG_SYMLINK)
{
shortcutInfo.TargetPath = NativeFileOperationsHelper.ParseSymLink(path);
}
}
}
itemType ??= isDirectory ? FilesystemItemType.Directory : FilesystemItemType.File;
}
else if (isHiddenItem)
{
itemType = NativeFileOperationsHelper.HasFileAttribute(path, System.IO.FileAttributes.Directory) ? FilesystemItemType.Directory : FilesystemItemType.File;
Expand Down Expand Up @@ -337,7 +353,7 @@ private static async Task<FilesystemResult> OpenFile(string path, IShellPage ass
{
var opened = (FilesystemResult)false;
bool isHiddenItem = NativeFileOperationsHelper.HasFileAttribute(path, System.IO.FileAttributes.Hidden);
bool isShortcutItem = path.EndsWith(".lnk") || path.EndsWith(".url"); // Determine
bool isShortcutItem = path.EndsWith(".lnk") || path.EndsWith(".url") || !string.IsNullOrEmpty(shortcutInfo.TargetPath);
if (isShortcutItem)
{
if (string.IsNullOrEmpty(shortcutInfo.TargetPath))
Expand Down
2 changes: 1 addition & 1 deletion Files/ViewModels/Properties/FileProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public override void GetBaseProperties()
ViewModel.ShortcutItemWorkingDirVisibility = Item.IsLinkItem ? Visibility.Collapsed : Visibility.Visible;
ViewModel.ShortcutItemArguments = shortcutItem.Arguments;
ViewModel.ShortcutItemArgumentsVisibility = Item.IsLinkItem ? Visibility.Collapsed : Visibility.Visible;
ViewModel.IsSelectedItemShortcut = Item.FileExtension.Equals(".lnk", StringComparison.OrdinalIgnoreCase);
ViewModel.IsSelectedItemShortcut = ".lnk".Equals(Item.FileExtension, StringComparison.OrdinalIgnoreCase);
ViewModel.ShortcutItemOpenLinkCommand = new RelayCommand(async () =>
{
if (Item.IsLinkItem)
Expand Down