ReparsePoint.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation 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.ComponentModel; 7 using System.IO; 8 using System.Runtime.InteropServices; 9 using System.Text; 10 11 using Microsoft.Win32.SafeHandles; 12 using Windows.Storage.Streams; 13 using Wox.Plugin; 14 15 namespace Microsoft.Plugin.Program.Utils 16 { 17 /// <summary> 18 /// Provides access to NTFS reparse points in .Net. 19 /// </summary> 20 public static class ReparsePoint 21 { 22 #pragma warning disable SA1310 // Field names should not contain underscore 23 24 private const int ERROR_NOT_A_REPARSE_POINT = 4390; 25 26 private const int ERROR_INSUFFICIENT_BUFFER = 122; 27 28 private const int ERROR_MORE_DATA = 234; 29 30 private const int FSCTL_GET_REPARSE_POINT = 0x000900A8; 31 32 private const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B; 33 34 private const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024; 35 36 private const int E_INVALID_PROTOCOL_FORMAT = unchecked((int)0x83760002); 37 #pragma warning restore SA1310 // Field names should not contain underscore 38 39 [Flags] 40 private enum FileAccessType : uint 41 { 42 DELETE = 0x00010000, 43 READ_CONTROL = 0x00020000, 44 WRITE_DAC = 0x00040000, 45 WRITE_OWNER = 0x00080000, 46 SYNCHRONIZE = 0x00100000, 47 48 STANDARD_RIGHTS_REQUIRED = 0x000F0000, 49 50 STANDARD_RIGHTS_READ = READ_CONTROL, 51 STANDARD_RIGHTS_WRITE = READ_CONTROL, 52 STANDARD_RIGHTS_EXECUTE = READ_CONTROL, 53 54 STANDARD_RIGHTS_ALL = 0x001F0000, 55 56 SPECIFIC_RIGHTS_ALL = 0x0000FFFF, 57 58 ACCESS_SYSTEM_SECURITY = 0x01000000, 59 60 MAXIMUM_ALLOWED = 0x02000000, 61 62 GENERIC_READ = 0x80000000, 63 GENERIC_WRITE = 0x40000000, 64 GENERIC_EXECUTE = 0x20000000, 65 GENERIC_ALL = 0x10000000, 66 67 FILE_READ_DATA = 0x0001, 68 FILE_WRITE_DATA = 0x0002, 69 FILE_APPEND_DATA = 0x0004, 70 FILE_READ_EA = 0x0008, 71 FILE_WRITE_EA = 0x0010, 72 FILE_EXECUTE = 0x0020, 73 FILE_READ_ATTRIBUTES = 0x0080, 74 FILE_WRITE_ATTRIBUTES = 0x0100, 75 76 FILE_ALL_ACCESS = 77 STANDARD_RIGHTS_REQUIRED | 78 SYNCHRONIZE 79 | 0x1FF, 80 81 FILE_GENERIC_READ = 82 STANDARD_RIGHTS_READ | 83 FILE_READ_DATA | 84 FILE_READ_ATTRIBUTES | 85 FILE_READ_EA | 86 SYNCHRONIZE, 87 88 FILE_GENERIC_WRITE = 89 STANDARD_RIGHTS_WRITE | 90 FILE_WRITE_DATA | 91 FILE_WRITE_ATTRIBUTES | 92 FILE_WRITE_EA | 93 FILE_APPEND_DATA | 94 SYNCHRONIZE, 95 96 FILE_GENERIC_EXECUTE = 97 STANDARD_RIGHTS_EXECUTE | 98 FILE_READ_ATTRIBUTES | 99 FILE_EXECUTE | 100 SYNCHRONIZE, 101 } 102 103 [Flags] 104 private enum FileShareType : uint 105 { 106 None = 0x00000000, 107 Read = 0x00000001, 108 Write = 0x00000002, 109 Delete = 0x00000004, 110 } 111 112 private enum CreationDisposition : uint 113 { 114 New = 1, 115 CreateAlways = 2, 116 OpenExisting = 3, 117 OpenAlways = 4, 118 TruncateExisting = 5, 119 } 120 121 [Flags] 122 private enum FileAttributes : uint 123 { 124 Readonly = 0x00000001, 125 Hidden = 0x00000002, 126 System = 0x00000004, 127 Directory = 0x00000010, 128 Archive = 0x00000020, 129 Device = 0x00000040, 130 Normal = 0x00000080, 131 Temporary = 0x00000100, 132 SparseFile = 0x00000200, 133 ReparsePoint = 0x00000400, 134 Compressed = 0x00000800, 135 Offline = 0x00001000, 136 NotContentIndexed = 0x00002000, 137 Encrypted = 0x00004000, 138 Write_Through = 0x80000000, 139 Overlapped = 0x40000000, 140 NoBuffering = 0x20000000, 141 RandomAccess = 0x10000000, 142 SequentialScan = 0x08000000, 143 DeleteOnClose = 0x04000000, 144 BackupSemantics = 0x02000000, 145 PosixSemantics = 0x01000000, 146 OpenReparsePoint = 0x00200000, 147 OpenNoRecall = 0x00100000, 148 FirstPipeInstance = 0x00080000, 149 } 150 151 private enum AppExecutionAliasReparseTagBufferLayoutVersion : uint 152 { 153 Invalid = 0, 154 155 /// <summary> 156 /// Initial version used package full name, aumid, exe path 157 /// </summary> 158 Initial = 1, 159 160 /// <summary> 161 /// This version replaces package full name with family name, to allow 162 /// optional packages to reference their main package across versions. 163 /// </summary> 164 PackageFamilyName = 2, 165 166 /// <summary> 167 /// This version appends a flag to the family Name version to differentiate 168 /// between UWP and Centennial 169 /// </summary> 170 MultiAppTypeSupport = 3, 171 172 /// <summary> 173 /// Used to check version validity, where valid is (Invalid, UpperBound) 174 /// </summary> 175 UpperBound, 176 } 177 178 [StructLayout(LayoutKind.Sequential)] 179 private struct AppExecutionAliasReparseTagHeader 180 { 181 /// <summary> 182 /// Reparse point tag. 183 /// </summary> 184 public uint ReparseTag; 185 186 /// <summary> 187 /// Size, in bytes, of the data after the Reserved member. 188 /// </summary> 189 public ushort ReparseDataLength; 190 191 /// <summary> 192 /// Reserved; do not use. 193 /// </summary> 194 public ushort Reserved; 195 196 public AppExecutionAliasReparseTagBufferLayoutVersion Version; 197 } 198 199 [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] 200 private static extern bool DeviceIoControl( 201 IntPtr hDevice, 202 uint dwIoControlCode, 203 IntPtr inBuffer, 204 int nInBufferSize, 205 IntPtr outBuffer, 206 int nOutBufferSize, 207 out int pBytesReturned, 208 IntPtr lpOverlapped); 209 210 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 211 private static extern IntPtr CreateFile( 212 string lpFileName, 213 FileAccessType dwDesiredAccess, 214 FileShareType dwShareMode, 215 IntPtr lpSecurityAttributes, 216 CreationDisposition dwCreationDisposition, 217 FileAttributes dwFlagsAndAttributes, 218 IntPtr hTemplateFile); 219 220 /// <summary> 221 /// Gets the target of the specified reparse point. 222 /// </summary> 223 /// <param name="reparsePoint">The path of the reparse point.</param> 224 /// <returns> 225 /// The target of the reparse point. 226 /// </returns> 227 /// <exception cref="IOException"> 228 /// Thrown when the reparse point specified is not a reparse point or is invalid. 229 /// </exception> 230 public static string GetTarget(string reparsePoint) 231 { 232 using (SafeFileHandle reparsePointHandle = new SafeFileHandle( 233 CreateFile( 234 reparsePoint, 235 FileAccessType.FILE_READ_ATTRIBUTES | FileAccessType.FILE_READ_EA, 236 FileShareType.Delete | FileShareType.Read | FileShareType.Write, 237 IntPtr.Zero, 238 CreationDisposition.OpenExisting, 239 FileAttributes.OpenReparsePoint, 240 IntPtr.Zero), 241 true)) 242 { 243 if (Marshal.GetLastWin32Error() != 0) 244 { 245 ThrowLastWin32Error("Unable to open reparse point."); 246 } 247 248 int outBufferSize = 512; 249 IntPtr outBuffer = Marshal.AllocHGlobal(outBufferSize); 250 251 try 252 { 253 // For-loop allows an attempt with 512-bytes buffer, before retrying with a 'MAXIMUM_REPARSE_DATA_BUFFER_SIZE' bytes buffer. 254 for (int i = 0; i < 2; ++i) 255 { 256 int bytesReturned; 257 bool result = DeviceIoControl( 258 reparsePointHandle.DangerousGetHandle(), 259 FSCTL_GET_REPARSE_POINT, 260 IntPtr.Zero, 261 0, 262 outBuffer, 263 outBufferSize, 264 out bytesReturned, 265 IntPtr.Zero); 266 267 if (!result) 268 { 269 int error = Marshal.GetLastWin32Error(); 270 if (error == ERROR_NOT_A_REPARSE_POINT) 271 { 272 return null; 273 } 274 275 if ((error == ERROR_INSUFFICIENT_BUFFER) || (error == ERROR_MORE_DATA)) 276 { 277 Marshal.FreeHGlobal(outBuffer); 278 outBuffer = IntPtr.Zero; 279 280 outBufferSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; 281 outBuffer = Marshal.AllocHGlobal(outBufferSize); 282 continue; 283 } 284 285 ThrowLastWin32Error("Unable to get information about reparse point."); 286 } 287 288 AppExecutionAliasReparseTagHeader aliasReparseHeader = Marshal.PtrToStructure<AppExecutionAliasReparseTagHeader>(outBuffer); 289 290 if (aliasReparseHeader.ReparseTag == IO_REPARSE_TAG_APPEXECLINK) 291 { 292 var metadata = AppExecutionAliasMetadata.FromPersistedRepresentationIntPtr( 293 outBuffer, 294 aliasReparseHeader.Version); 295 296 return metadata.ExePath; 297 } 298 299 return null; 300 } 301 } 302 finally 303 { 304 Marshal.FreeHGlobal(outBuffer); 305 } 306 } 307 308 return null; 309 } 310 311 private class AppExecutionAliasMetadata 312 { 313 public string PackageFullName { get; init; } 314 315 public string PackageFamilyName { get; init; } 316 317 public string Aumid { get; init; } 318 319 public string ExePath { get; init; } 320 321 public static AppExecutionAliasMetadata FromPersistedRepresentationIntPtr(IntPtr reparseDataBufferPtr, AppExecutionAliasReparseTagBufferLayoutVersion version) 322 { 323 var dataOffset = Marshal.SizeOf(typeof(AppExecutionAliasReparseTagHeader)); 324 IntPtr dataBufferPtr = reparseDataBufferPtr + dataOffset; 325 326 string packageFullName = null; 327 string packageFamilyName = null; 328 string aumid = null; 329 string exePath = null; 330 331 VerifyVersion(version); 332 333 switch (version) 334 { 335 case AppExecutionAliasReparseTagBufferLayoutVersion.Initial: 336 packageFullName = Marshal.PtrToStringUni(dataBufferPtr); 337 dataBufferPtr += Encoding.Unicode.GetByteCount(packageFullName) + Encoding.Unicode.GetByteCount("\0"); 338 339 aumid = Marshal.PtrToStringUni(dataBufferPtr); 340 dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0"); 341 342 exePath = Marshal.PtrToStringUni(dataBufferPtr); 343 break; 344 345 case AppExecutionAliasReparseTagBufferLayoutVersion.PackageFamilyName: 346 case AppExecutionAliasReparseTagBufferLayoutVersion.MultiAppTypeSupport: 347 packageFamilyName = Marshal.PtrToStringUni(dataBufferPtr); 348 dataBufferPtr += Encoding.Unicode.GetByteCount(packageFamilyName) + Encoding.Unicode.GetByteCount("\0"); 349 350 aumid = Marshal.PtrToStringUni(dataBufferPtr); 351 dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0"); 352 353 exePath = Marshal.PtrToStringUni(dataBufferPtr); 354 break; 355 } 356 357 return new AppExecutionAliasMetadata 358 { 359 PackageFullName = packageFullName, 360 PackageFamilyName = packageFamilyName, 361 Aumid = aumid, 362 ExePath = exePath, 363 }; 364 } 365 366 private static void VerifyVersion(AppExecutionAliasReparseTagBufferLayoutVersion version) 367 { 368 uint uintVersion = (uint)version; 369 370 if (uintVersion > (uint)AppExecutionAliasReparseTagBufferLayoutVersion.Invalid && 371 uintVersion < (uint)AppExecutionAliasReparseTagBufferLayoutVersion.UpperBound) 372 { 373 return; 374 } 375 376 throw new IOException("Invalid app execution alias reparse version.", E_INVALID_PROTOCOL_FORMAT); 377 } 378 } 379 380 private static void ThrowLastWin32Error(string message) 381 { 382 throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())); 383 } 384 } 385 }