[PTRun][Program]Improve the icon shown for app execution aliases(#20358)

* Improve the icon shown for app execution aliases.

* Reverse validation check.
Resolve issue with null paths

* Improve to utilise app execution alias reparse point.

* Improve to utilise app execution alias reparse point.

Co-authored-by: Mike Barker <mibarker@microsoft.com>
This commit is contained in:
Mike Barker 2022-09-20 18:40:25 +01:00 committed by GitHub
parent 789312c6ce
commit 28e6545f6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 421 additions and 0 deletions

View File

@ -15,6 +15,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Plugin.Program.Logger;
using Microsoft.Plugin.Program.Utils;
using Microsoft.Win32;
using Wox.Infrastructure;
using Wox.Infrastructure.FileSystemHelper;
@ -916,10 +917,46 @@ namespace Microsoft.Plugin.Program.Programs
}
}
private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string icoPath)
{
if (program.AppType != ApplicationType.RunCommand)
{
icoPath = null;
return false;
}
if (string.IsNullOrEmpty(program.FullPath))
{
icoPath = null;
return false;
}
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
try
{
var redirectionPath = ReparsePoint.GetTarget(program.FullPath);
icoPath = ExpandEnvironmentVariables(redirectionPath);
return true;
}
catch (IOException e)
{
ProgramLogger.Warn($"|Error whilst retrieving the redirection path from app execution alias {program.FullPath}", e, MethodBase.GetCurrentMethod().DeclaringType, program.FullPath);
}
icoPath = null;
return false;
}
private static Win32Program GetRunCommandProgramFromPath(string path)
{
var program = GetProgramFromPath(path);
program.AppType = ApplicationType.RunCommand;
if (TryGetIcoPathForRunCommandProgram(program, out var icoPath))
{
program.IcoPath = icoPath;
}
return program;
}

View File

@ -0,0 +1,384 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using Windows.Storage.Streams;
using Wox.Plugin;
namespace Microsoft.Plugin.Program.Utils
{
/// <summary>
/// Provides access to NTFS reparse points in .Net.
/// </summary>
public static class ReparsePoint
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const int ERROR_NOT_A_REPARSE_POINT = 4390;
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private const int ERROR_MORE_DATA = 234;
private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
private const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B;
private const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
private const int E_INVALID_PROTOCOL_FORMAT = unchecked((int)0x83760002);
#pragma warning restore SA1310 // Field names should not contain underscore
[Flags]
private enum FileAccessType : uint
{
DELETE = 0x00010000,
READ_CONTROL = 0x00020000,
WRITE_DAC = 0x00040000,
WRITE_OWNER = 0x00080000,
SYNCHRONIZE = 0x00100000,
STANDARD_RIGHTS_REQUIRED = 0x000F0000,
STANDARD_RIGHTS_READ = READ_CONTROL,
STANDARD_RIGHTS_WRITE = READ_CONTROL,
STANDARD_RIGHTS_EXECUTE = READ_CONTROL,
STANDARD_RIGHTS_ALL = 0x001F0000,
SPECIFIC_RIGHTS_ALL = 0x0000FFFF,
ACCESS_SYSTEM_SECURITY = 0x01000000,
MAXIMUM_ALLOWED = 0x02000000,
GENERIC_READ = 0x80000000,
GENERIC_WRITE = 0x40000000,
GENERIC_EXECUTE = 0x20000000,
GENERIC_ALL = 0x10000000,
FILE_READ_DATA = 0x0001,
FILE_WRITE_DATA = 0x0002,
FILE_APPEND_DATA = 0x0004,
FILE_READ_EA = 0x0008,
FILE_WRITE_EA = 0x0010,
FILE_EXECUTE = 0x0020,
FILE_READ_ATTRIBUTES = 0x0080,
FILE_WRITE_ATTRIBUTES = 0x0100,
FILE_ALL_ACCESS =
STANDARD_RIGHTS_REQUIRED |
SYNCHRONIZE
| 0x1FF,
FILE_GENERIC_READ =
STANDARD_RIGHTS_READ |
FILE_READ_DATA |
FILE_READ_ATTRIBUTES |
FILE_READ_EA |
SYNCHRONIZE,
FILE_GENERIC_WRITE =
STANDARD_RIGHTS_WRITE |
FILE_WRITE_DATA |
FILE_WRITE_ATTRIBUTES |
FILE_WRITE_EA |
FILE_APPEND_DATA |
SYNCHRONIZE,
FILE_GENERIC_EXECUTE =
STANDARD_RIGHTS_EXECUTE |
FILE_READ_ATTRIBUTES |
FILE_EXECUTE |
SYNCHRONIZE,
}
[Flags]
private enum FileShareType : uint
{
None = 0x00000000,
Read = 0x00000001,
Write = 0x00000002,
Delete = 0x00000004,
}
private enum CreationDisposition : uint
{
New = 1,
CreateAlways = 2,
OpenExisting = 3,
OpenAlways = 4,
TruncateExisting = 5,
}
[Flags]
private enum FileAttributes : uint
{
Readonly = 0x00000001,
Hidden = 0x00000002,
System = 0x00000004,
Directory = 0x00000010,
Archive = 0x00000020,
Device = 0x00000040,
Normal = 0x00000080,
Temporary = 0x00000100,
SparseFile = 0x00000200,
ReparsePoint = 0x00000400,
Compressed = 0x00000800,
Offline = 0x00001000,
NotContentIndexed = 0x00002000,
Encrypted = 0x00004000,
Write_Through = 0x80000000,
Overlapped = 0x40000000,
NoBuffering = 0x20000000,
RandomAccess = 0x10000000,
SequentialScan = 0x08000000,
DeleteOnClose = 0x04000000,
BackupSemantics = 0x02000000,
PosixSemantics = 0x01000000,
OpenReparsePoint = 0x00200000,
OpenNoRecall = 0x00100000,
FirstPipeInstance = 0x00080000,
}
private enum AppExecutionAliasReparseTagBufferLayoutVersion : uint
{
Invalid = 0,
/// <summary>
/// Initial version used package full name, aumid, exe path
/// </summary>
Initial = 1,
/// <summary>
/// This version replaces package full name with family name, to allow
/// optional packages to reference their main package across versions.
/// </summary>
PackageFamilyName = 2,
/// <summary>
/// This version appends a flag to the family Name version to differentiate
/// between UWP and Centennial
/// </summary>
MultiAppTypeSupport = 3,
/// <summary>
/// Used to check version validity, where valid is (Invalid, UpperBound)
/// </summary>
UpperBound,
}
[StructLayout(LayoutKind.Sequential)]
private struct AppExecutionAliasReparseTagHeader
{
/// <summary>
/// Reparse point tag.
/// </summary>
public uint ReparseTag;
/// <summary>
/// Size, in bytes, of the data after the Reserved member.
/// </summary>
public ushort ReparseDataLength;
/// <summary>
/// Reserved; do not use.
/// </summary>
public ushort Reserved;
public AppExecutionAliasReparseTagBufferLayoutVersion Version;
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool DeviceIoControl(
IntPtr hDevice,
uint dwIoControlCode,
IntPtr inBuffer,
int nInBufferSize,
IntPtr outBuffer,
int nOutBufferSize,
out int pBytesReturned,
IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreateFile(
string lpFileName,
FileAccessType dwDesiredAccess,
FileShareType dwShareMode,
IntPtr lpSecurityAttributes,
CreationDisposition dwCreationDisposition,
FileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
/// <summary>
/// Gets the target of the specified reparse point.
/// </summary>
/// <param name="reparsePoint">The path of the reparse point.</param>
/// <returns>
/// The target of the reparse point.
/// </returns>
/// <exception cref="IOException">
/// Thrown when the reparse point specified is not a reparse point or is invalid.
/// </exception>
public static string GetTarget(string reparsePoint)
{
using (SafeFileHandle reparsePointHandle = new SafeFileHandle(
CreateFile(
reparsePoint,
FileAccessType.FILE_READ_ATTRIBUTES | FileAccessType.FILE_READ_EA,
FileShareType.Delete | FileShareType.Read | FileShareType.Write,
IntPtr.Zero,
CreationDisposition.OpenExisting,
FileAttributes.OpenReparsePoint,
IntPtr.Zero),
true))
{
if (Marshal.GetLastWin32Error() != 0)
{
ThrowLastWin32Error("Unable to open reparse point.");
}
int outBufferSize = 512;
IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize);
try
{
// For-loop allows an attempt with 512-bytes buffer, before retrying with a 'MAXIMUM_REPARSE_DATA_BUFFER_SIZE' bytes buffer.
for (int i = 0; i < 2; ++i)
{
int bytesReturned;
bool result = DeviceIoControl(
reparsePointHandle.DangerousGetHandle(),
FSCTL_GET_REPARSE_POINT,
IntPtr.Zero,
0,
outBuffer,
outBufferSize,
out bytesReturned,
IntPtr.Zero);
if (!result)
{
int error = Marshal.GetLastWin32Error();
if (error == ERROR_NOT_A_REPARSE_POINT)
{
return null;
}
if ((error == ERROR_INSUFFICIENT_BUFFER) || (error == ERROR_MORE_DATA))
{
Marshal.FreeHGlobal(outBuffer);
outBuffer = IntPtr.Zero;
outBufferSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE;
outBuffer = Marshal.AllocHGlobal(outBufferSize);
continue;
}
ThrowLastWin32Error("Unable to get information about reparse point.");
}
AppExecutionAliasReparseTagHeader aliasReparseHeader = Marshal.PtrToStructure<AppExecutionAliasReparseTagHeader>(outBuffer);
if (aliasReparseHeader.ReparseTag == IO_REPARSE_TAG_APPEXECLINK)
{
var metadata = AppExecutionAliasMetadata.FromPersistedRepresentationIntPtr(
outBuffer,
aliasReparseHeader.Version);
return metadata.ExePath;
}
return null;
}
}
finally
{
Marshal.FreeHGlobal(outBuffer);
}
}
return null;
}
private class AppExecutionAliasMetadata
{
public string PackageFullName { get; init; }
public string PackageFamilyName { get; init; }
public string Aumid { get; init; }
public string ExePath { get; init; }
public static AppExecutionAliasMetadata FromPersistedRepresentationIntPtr(IntPtr reparseDataBufferPtr, AppExecutionAliasReparseTagBufferLayoutVersion version)
{
var dataOffset = Marshal.SizeOf(typeof(AppExecutionAliasReparseTagHeader));
IntPtr dataBufferPtr = reparseDataBufferPtr + dataOffset;
string packageFullName = null;
string packageFamilyName = null;
string aumid = null;
string exePath = null;
VerifyVersion(version);
switch (version)
{
case AppExecutionAliasReparseTagBufferLayoutVersion.Initial:
packageFullName = Marshal.PtrToStringUni(dataBufferPtr);
dataBufferPtr += Encoding.Unicode.GetByteCount(packageFullName) + Encoding.Unicode.GetByteCount("\0");
aumid = Marshal.PtrToStringUni(dataBufferPtr);
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0");
exePath = Marshal.PtrToStringUni(dataBufferPtr);
break;
case AppExecutionAliasReparseTagBufferLayoutVersion.PackageFamilyName:
case AppExecutionAliasReparseTagBufferLayoutVersion.MultiAppTypeSupport:
packageFamilyName = Marshal.PtrToStringUni(dataBufferPtr);
dataBufferPtr += Encoding.Unicode.GetByteCount(packageFamilyName) + Encoding.Unicode.GetByteCount("\0");
aumid = Marshal.PtrToStringUni(dataBufferPtr);
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0");
exePath = Marshal.PtrToStringUni(dataBufferPtr);
break;
}
return new AppExecutionAliasMetadata
{
PackageFullName = packageFullName,
PackageFamilyName = packageFamilyName,
Aumid = aumid,
ExePath = exePath,
};
}
private static void VerifyVersion(AppExecutionAliasReparseTagBufferLayoutVersion version)
{
uint uintVersion = (uint)version;
if (uintVersion > (uint)AppExecutionAliasReparseTagBufferLayoutVersion.Invalid &&
uintVersion < (uint)AppExecutionAliasReparseTagBufferLayoutVersion.UpperBound)
{
return;
}
throw new IOException("Invalid app execution alias reparse version.", E_INVALID_PROTOCOL_FORMAT);
}
}
private static void ThrowLastWin32Error(string message)
{
throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
}
}
}