src/Tasks/Microsoft.NET.Build.Tasks/ResourceUpdater.cs (319 lines of code) (raw):

// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.IO; using System.Diagnostics; using System.Runtime.InteropServices; namespace Microsoft.NET.Build.Tasks { /// <summary> /// Provides methods for modifying the embedded native resources /// in a PE image. It currently only works on Windows, because it /// requires various kernel32 APIs. /// </summary> internal class ResourceUpdater : IDisposable { private sealed class Kernel32 { // // Native methods for updating resources // [DllImport(nameof(Kernel32), SetLastError=true)] public static extern SafeUpdateHandle BeginUpdateResource(string pFileName, [MarshalAs(UnmanagedType.Bool)]bool bDeleteExistingResources); // Update a resource with data from an IntPtr [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool UpdateResource(SafeUpdateHandle hUpdate, IntPtr lpType, IntPtr lpName, ushort wLanguage, IntPtr lpData, uint cbData); // Update a resource with data from a managed byte[] [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool UpdateResource(SafeUpdateHandle hUpdate, IntPtr lpType, IntPtr lpName, ushort wLanguage, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData, uint cbData); [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EndUpdateResource(SafeUpdateHandle hUpdate, bool fDiscard); // The IntPtr version of this dllimport is used in the // SafeHandle implementation [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EndUpdateResource(IntPtr hUpdate, bool fDiscard); public const ushort LangID_LangNeutral_SublangNeutral = 0; // // Native methods used to read resources from a PE file // // Loading and freeing PE files public enum LoadLibraryFlags : uint { LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020 } [DllImport(nameof(Kernel32), SetLastError=true)] public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, LoadLibraryFlags dwFlags); [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool FreeLibrary(IntPtr hModule); // Enumerating resources public delegate bool EnumResTypeProc(IntPtr hModule, IntPtr lpType, IntPtr lParam); public delegate bool EnumResNameProc(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam); public delegate bool EnumResLangProc(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam); [DllImport(nameof(Kernel32),SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumResourceTypes(IntPtr hModule, EnumResTypeProc lpEnumFunc, IntPtr lParam); [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumResourceNames(IntPtr hModule, IntPtr lpType, EnumResNameProc lpEnumFunc, IntPtr lParam); [DllImport(nameof(Kernel32), SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumResourceLanguages(IntPtr hModule, IntPtr lpType, IntPtr lpName, EnumResLangProc lpEnumFunc, IntPtr lParam); public const int UserStoppedResourceEnumerationHRESULT = unchecked((int)0x80073B02); public const int ResourceDataNotFoundHRESULT = unchecked((int)0x80070714); // Querying and loading resources [DllImport(nameof(Kernel32), SetLastError=true)] public static extern IntPtr FindResourceEx(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLanguage); [DllImport(nameof(Kernel32), SetLastError=true)] public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo); [DllImport(nameof(Kernel32))] // does not call SetLastError public static extern IntPtr LockResource(IntPtr hResData); [DllImport(nameof(Kernel32), SetLastError=true)] public static extern uint SizeofResource(IntPtr hModule, IntPtr hResInfo); } /// <summary> /// Holds the update handle returned by BeginUpdateResource. /// Normally, native resources for the update handle are /// released by a call to ResourceUpdater.Update(). In case /// this doesn't happen, the SafeUpdateHandle will release the /// native resources for the update handle without updating /// the target file. /// </summary> private class SafeUpdateHandle : SafeHandle { private SafeUpdateHandle() : base(IntPtr.Zero, true) { } public override bool IsInvalid => handle == IntPtr.Zero; protected override bool ReleaseHandle() { // discard pending updates without writing them return Kernel32.EndUpdateResource(handle, true); } } /// <summary> /// Holds the native handle for the resource update. /// </summary> private readonly SafeUpdateHandle hUpdate; ///<summary> /// Determines if the ResourceUpdater is supported by the current operating system. /// Some versions of Windows, such as Nano Server, do not support the needed APIs. /// </summary> public static bool IsSupportedOS() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return false; } try { // On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error // Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported using (var handle = Kernel32.BeginUpdateResource("", false)) { if (handle.IsInvalid && Marshal.GetLastWin32Error() == 0) { return false; } } } catch (EntryPointNotFoundException) { // BeginUpdateResource isn't exported from Kernel32 return false; } return true; } /// <summary> /// Create a resource updater for the given PE file. This will /// acquire a native resource update handle for the file, /// preparing it for updates. Resources can be added to this /// updater, which will queue them for update. The target PE /// file will not be modified until Update() is called, after /// which the ResourceUpdater can not be used for further /// updates. /// </summary> public ResourceUpdater(string peFile) { hUpdate = Kernel32.BeginUpdateResource(peFile, false); if (hUpdate.IsInvalid) { ThrowExceptionForLastWin32Error(); } } /// <summary> /// Add all resources from a source PE file. It is assumed /// that the input is a valid PE file. If it is not, an /// exception will be thrown. This will not modify the target /// until Update() is called. /// Throws an InvalidOperationException if Update() was already called. /// </summary> public ResourceUpdater AddResourcesFromPEImage(string peFile) { if (hUpdate.IsInvalid) { ThrowExceptionForInvalidUpdate(); } // Using both flags lets the OS loader decide how to load // it most efficiently. Either mode will prevent other // processes from modifying the module while it is loaded. IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero, Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE | Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE); if (hModule == IntPtr.Zero) { ThrowExceptionForLastWin32Error(); } var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback); var errorInfo = new EnumResourcesErrorInfo(); GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo); var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle); try { if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr)) { if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT) { CaptureEnumResourcesErrorInfo(errorInfoPtr); errorInfo.ThrowException(); } } } finally { errorInfoHandle.Free(); if (!Kernel32.FreeLibrary(hModule)) { ThrowExceptionForLastWin32Error(); } } return this; } private static bool IsIntResource(IntPtr lpType) { return ((uint)lpType >> 16) == 0; } /// <summary> /// Add a language-neutral integer resource from a byte[] with /// a particular type and name. This will not modify the /// target until Update() is called. /// Throws an InvalidOperationException if Update() was already called. /// </summary> public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName) { if (hUpdate.IsInvalid) { ThrowExceptionForInvalidUpdate(); } if (!IsIntResource(lpType) || !IsIntResource(lpName)) { throw new ArgumentException(Strings.AddResourceWithNonIntegerResource); } if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length)) { ThrowExceptionForLastWin32Error(); } return this; } /// <summary> /// Write the pending resource updates to the target PE /// file. After this, the ResourceUpdater no longer maintains /// an update handle, and can not be used for further updates. /// Throws an InvalidOperationException if Update() was already called. /// </summary> public void Update() { if (hUpdate.IsInvalid) { ThrowExceptionForInvalidUpdate(); } try { if (!Kernel32.EndUpdateResource(hUpdate, false)) { ThrowExceptionForLastWin32Error(); } } finally { hUpdate.SetHandleAsInvalid(); } } private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam) { var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback); if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam)) { CaptureEnumResourcesErrorInfo(lParam); return false; } return true; } private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam) { var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback); if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam)) { CaptureEnumResourcesErrorInfo(lParam); return false; } return true; } private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam) { IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang); if (hResource == IntPtr.Zero) { CaptureEnumResourcesErrorInfo(lParam); return false; } // hResourceLoaded is just a handle to the resource, which // can be used to get the resource data IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource); if (hResourceLoaded == IntPtr.Zero) { CaptureEnumResourcesErrorInfo(lParam); return false; } // This doesn't actually lock memory. It just retrieves a // pointer to the resource data. The pointer is valid // until the module is unloaded. IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded); if (lpResourceData == IntPtr.Zero) { ((EnumResourcesErrorInfo)GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true; } if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource))) { CaptureEnumResourcesErrorInfo(lParam); return false; } return true; } private class EnumResourcesErrorInfo { public int hResult; public bool failedToLockResource; public void ThrowException() { if (failedToLockResource) { Debug.Assert(hResult == 0); throw new ResourceNotAvailableException(Strings.FailedToLockResource); } Debug.Assert(hResult != 0); throw new HResultException(hResult); } } private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr) { int hResult = Marshal.GetHRForLastWin32Error(); if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT) { GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr); var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target; errorInfo.hResult = hResult; } } private class ResourceNotAvailableException : Exception { public ResourceNotAvailableException(string message) : base(message) { } } private class HResultException : Exception { public HResultException(int hResult) : base(hResult.ToString("X4")) { } } private static void ThrowExceptionForLastWin32Error() { throw new HResultException(Marshal.GetHRForLastWin32Error()); } private static void ThrowExceptionForInvalidUpdate() { throw new InvalidOperationException(Strings.InvalidResourceUpdate); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose(bool disposing) { if (disposing) { hUpdate.Dispose(); } } } }