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  }