This enables cross-builds of windows applications on non-windows to have updated win32 resources. It also removes the need to open/write the app host multiple times during build.
namespace ILCompiler.DependencyAnalysis
{
+ // There is small set of ObjectDataBuilder in at src/installer/managed/Microsoft.NET.HostModel/ObjectDataBuilder.cs
+ // only for ResourceData.WriteResources
public struct ObjectDataBuilder
#if !READYTORUN
: Internal.Runtime.ITargetBinaryWriter
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
namespace ILCompiler.Win32Resources
+#endif
{
public unsafe partial class ResourceData
{
if (!resourceFilter(typeName, name, (ushort)languageName))
return;
}
- AddResource(typeName, name, (ushort)languageName, data);
+ AddResourceInternal(name, typeName, (ushort)languageName, data);
}
}
}
using System;
using System.Collections.Generic;
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
namespace ILCompiler.Win32Resources
+#endif
{
public unsafe partial class ResourceData
{
using System;
using System.Collections;
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
namespace ILCompiler.Win32Resources
+#endif
{
public unsafe partial class ResourceData
{
- private void AddResource(object type, object name, ushort language, byte[] data)
+ private void AddResourceInternal(object name, object type, ushort language, byte[] data)
{
ResType resType;
using System.Runtime.InteropServices;
using System.Reflection.Metadata;
+#if !HOST_MODEL
using ILCompiler.DependencyAnalysis;
+#endif
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
namespace ILCompiler.Win32Resources
+#endif
{
public unsafe partial class ResourceData
{
CodePage = blobReader.ReadUInt32();
Reserved = blobReader.ReadUInt32();
}
-
+#if HOST_MODEL
+ public static void Write(ref ObjectDataBuilder dataBuilder, int sectionBase, int offsetFromSymbol, int sizeOfData)
+#else
public static void Write(ref ObjectDataBuilder dataBuilder, ISymbolNode node, int offsetFromSymbol, int sizeOfData)
+#endif
{
+#if HOST_MODEL
+ dataBuilder.EmitInt(sectionBase + offsetFromSymbol);
+#else
dataBuilder.EmitReloc(node,
#if READYTORUN
RelocType.IMAGE_REL_BASED_ADDR32NB,
RelocType.IMAGE_REL_BASED_ABSOLUTE,
#endif
offsetFromSymbol);
+#endif
dataBuilder.EmitInt(sizeOfData);
dataBuilder.EmitInt(1252); // CODEPAGE = DEFAULT_CODEPAGE
dataBuilder.EmitInt(0); // RESERVED
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
+#if !HOST_MODEL
using ILCompiler.DependencyAnalysis;
-using Internal.TypeSystem.Ecma;
+#endif
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
namespace ILCompiler.Win32Resources
+#endif
{
/// <summary>
/// Resource abstraction to allow examination
/// </summary>
public unsafe partial class ResourceData
{
+#if HOST_MODEL
+ /// <summary>
+ /// Initialize a ResourceData instance from a PE file
+ /// </summary>
+ /// <param name="peFile"></param>
+ public ResourceData(PEReader peFile)
+ {
+ DirectoryEntry resourceDirectory = peFile.PEHeaders.PEHeader!.ResourceTableDirectory;
+ if (resourceDirectory.Size != 0)
+ {
+ BlobReader resourceDataBlob = peFile.GetSectionData(resourceDirectory.RelativeVirtualAddress).GetReader(0, resourceDirectory.Size);
+ ReadResourceData(resourceDataBlob, peFile, null);
+ }
+ }
+#else
/// <summary>
/// Initialize a ResourceData instance from a PE file
/// </summary>
/// <param name="ecmaModule"></param>
- public ResourceData(EcmaModule ecmaModule, Func<object, object, ushort, bool> resourceFilter = null)
+ public ResourceData(Internal.TypeSystem.Ecma.EcmaModule ecmaModule, Func<object, object, ushort, bool> resourceFilter = null)
{
System.Collections.Immutable.ImmutableArray<byte> ecmaData = ecmaModule.PEReader.GetEntireImage().GetContent();
PEReader peFile = ecmaModule.PEReader;
ReadResourceData(resourceDataBlob, peFile, resourceFilter);
}
}
+#endif
/// <summary>
/// Find a resource in the resource data
return FindResourceInternal(name, type, language);
}
+ /// <summary>
+ /// Add or update resource
+ /// </summary>
+ public void AddResource(string name, string type, ushort language, byte[] data) => AddResourceInternal(name, type, language, data);
+
+ /// <summary>
+ /// Add or update resource
+ /// </summary>
+ public void AddResource(string name, ushort type, ushort language, byte[] data) => AddResourceInternal(name, type, language, data);
+
+ /// <summary>
+ /// Add or update resource
+ /// </summary>
+ public void AddResource(ushort name, string type, ushort language, byte[] data) => AddResourceInternal(name, type, language, data);
+
+ /// <summary>
+ /// Add or update resource
+ /// </summary>
+ public void AddResource(ushort name, ushort type, ushort language, byte[] data) => AddResourceInternal(name, type, language, data);
+
+ public IEnumerable<(object name, object type, ushort language, byte[] data)> GetAllResources()
+ {
+ return _resTypeHeadID.SelectMany(typeIdPair => SelectResType(typeIdPair.Key, typeIdPair.Value))
+ .Concat(_resTypeHeadName.SelectMany(typeNamePair => SelectResType(typeNamePair.Key, typeNamePair.Value)));
+
+ IEnumerable<(object name, object type, ushort language, byte[] data)> SelectResType(object type, ResType resType)
+ {
+ return resType.NameHeadID.SelectMany(nameIdPair => SelectResName(type, nameIdPair.Key, nameIdPair.Value))
+ .Concat(resType.NameHeadName.SelectMany(nameNamePair =>
+ SelectResName(type, nameNamePair.Key, nameNamePair.Value)));
+ }
+
+ IEnumerable<(object name, object type, ushort language, byte[] data)> SelectResName(object type, object name, ResName resType)
+ {
+ return resType.Languages.Select((lang) => (name, type, lang.Key, lang.Value.DataEntry));
+ }
+ }
+
public bool IsEmpty
{
get
}
}
+ /// <summary>
+ /// Add all resources in the specified ResourceData struct.
+ /// </summary>
+ public void CopyResourcesFrom(ResourceData moduleResources)
+ {
+ foreach ((object name, object type, ushort language, byte[] data) in moduleResources.GetAllResources())
+ AddResourceInternal(name, type, language, data);
+ }
+
+#if HOST_MODEL
+ public void WriteResources(int sectionBase, ref ObjectDataBuilder dataBuilder)
+ {
+ WriteResources(sectionBase, ref dataBuilder, ref dataBuilder);
+ }
+#else
public void WriteResources(ISymbolNode nodeAssociatedWithDataBuilder, ref ObjectDataBuilder dataBuilder)
{
WriteResources(nodeAssociatedWithDataBuilder, ref dataBuilder, ref dataBuilder);
}
+#endif
+#if HOST_MODEL
+ public void WriteResources(int sectionBase, ref ObjectDataBuilder dataBuilder, ref ObjectDataBuilder contentBuilder)
+#else
public void WriteResources(ISymbolNode nodeAssociatedWithDataBuilder, ref ObjectDataBuilder dataBuilder, ref ObjectDataBuilder contentBuilder)
+#endif
{
Debug.Assert(dataBuilder.CountBytes == 0);
foreach (Tuple<ResLanguage, ObjectDataBuilder.Reservation> language in resLanguages)
{
dataBuilder.EmitInt(language.Item2, dataBuilder.CountBytes);
+#if HOST_MODEL
+ IMAGE_RESOURCE_DATA_ENTRY.Write(ref dataBuilder, sectionBase, dataEntryTable[language.Item1], language.Item1.DataEntry.Length);
+#else
IMAGE_RESOURCE_DATA_ENTRY.Write(ref dataBuilder, nodeAssociatedWithDataBuilder, dataEntryTable[language.Item1], language.Item1.DataEntry.Length);
+#endif
}
dataBuilder.PadAlignment(4); // resource data entries are 4 byte aligned
}
}
}
- void UpdateResources()
- {
- if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
- {
- if (ResourceUpdater.IsSupportedOS())
- {
- // Copy resources from managed dll to the apphost
- new ResourceUpdater(appHostDestinationFilePath)
- .AddResourcesFromPEImage(assemblyToCopyResourcesFrom)
- .Update();
- }
- else
- {
- throw new AppHostCustomizationUnsupportedOSException();
- }
- }
- }
-
try
{
RetryUtil.RetryOnIOError(() =>
{
MachOUtils.RemoveSignature(fileStream);
}
+
+ if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
+ {
+ using var updater = new ResourceUpdater(fileStream, true);
+ updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
+ updater.Update();
+ }
}
}
finally
}
});
- RetryUtil.RetryOnWin32Error(UpdateResources);
-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.IO;
using System.IO.MemoryMappedFiles;
public static class PEUtils
{
/// <summary>
- /// The first two bytes of a PE file are a constant signature.
- /// </summary>
- private const ushort PEFileSignature = 0x5A4D;
-
- /// <summary>
- /// The offset of the PE header pointer in the DOS header.
- /// </summary>
- private const int PEHeaderPointerOffset = 0x3C;
-
- /// <summary>
- /// The offset of the Subsystem field in the PE header.
- /// </summary>
- private const int SubsystemOffset = 0x5C;
-
- /// <summary>
- /// The value of the subsystem field which indicates Windows GUI (Graphical UI)
- /// </summary>
- private const ushort WindowsGUISubsystem = 0x2;
-
- /// <summary>
- /// The value of the subsystem field which indicates Windows CUI (Console)
- /// </summary>
- private const ushort WindowsCUISubsystem = 0x3;
-
- /// <summary>
/// Check whether the apphost file is a windows PE image by looking at the first few bytes.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
// https://en.wikipedia.org/wiki/Portable_Executable
// Validate that we're looking at Windows PE file
- if (((ushort*)bytes)[0] != PEFileSignature || accessor.Capacity < PEHeaderPointerOffset + sizeof(uint))
+ if (((ushort*)bytes)[0] != PEOffsets.DosImageSignature
+ || accessor.Capacity < PEOffsets.DosStub.PESignatureOffset + sizeof(uint))
{
return false;
}
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
- if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint))
+ if (reader.BaseStream.Length < PEOffsets.DosStub.PESignatureOffset + sizeof(uint))
{
return false;
}
ushort signature = reader.ReadUInt16();
- return signature == PEFileSignature;
+ return signature == PEOffsets.DosImageSignature;
}
}
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
- uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
+ uint peHeaderOffset = ((uint*)(bytes + PEOffsets.DosStub.PESignatureOffset))[0];
- if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
+ if (accessor.Capacity < peHeaderOffset + PEOffsets.PEHeader.Subsystem + sizeof(ushort))
{
throw new AppHostNotPEFileException("Subsystem offset out of file range.");
}
- ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
+ ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + PEOffsets.PEHeader.Subsystem));
// https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#windows-subsystem
// The subsystem of the prebuilt apphost should be set to CUI
- if (subsystem[0] != WindowsCUISubsystem)
+ if (subsystem[0] != (ushort)PEOffsets.Subsystem.WindowsCui)
{
throw new AppHostNotCUIException(subsystem[0]);
}
// Set the subsystem to GUI
- subsystem[0] = WindowsGUISubsystem;
+ subsystem[0] = (ushort)PEOffsets.Subsystem.WindowsGui;
}
finally
{
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
- uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
+ uint peHeaderOffset = ((uint*)(bytes + PEOffsets.DosStub.PESignatureOffset))[0];
- if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
+ if (accessor.Capacity < peHeaderOffset + PEOffsets.PEHeader.Subsystem + sizeof(ushort))
{
throw new AppHostNotPEFileException("Subsystem offset out of file range.");
}
- ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
+ ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + PEOffsets.PEHeader.Subsystem));
return subsystem[0];
}
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId Condition="'$(PgoInstrument)' == 'true'">Microsoft.Net.HostModel.PGO</PackageId>
+ <DefineConstants>$(DefineConstants);HOST_MODEL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
</ItemGroup>
+ <ItemGroup>
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\Compiler\Win32Resources\ResourceData.cs" Link="Win32Resources\ResourceData.cs" />
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\Compiler\Win32Resources\ResourceData.Reader.cs" Link="Win32Resources\ResourceData.Reader.cs" />
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\Compiler\Win32Resources\ResourceData.ResourcesDataModel.cs" Link="Win32Resources\ResourceData.ResourcesDataModel.cs" />
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\Compiler\Win32Resources\ResourceData.UpdateResourceDataModel.cs" Link="Win32Resources\ResourceData.UpdateResourceDataModel.cs" />
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\Compiler\Win32Resources\ResourceData.Win32Structs.cs" Link="Win32Resources\ResourceData.Win32Structs.cs" />
+
+ <Compile Include="$(CoreClrProjectRoot)tools\Common\System\Collections\Generic\ArrayBuilder.cs" Link="Common\ArrayBuilder.cs" />
+ </ItemGroup>
+
+
</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel;
+
+/// <summary>
+/// Offsets and constants of PE file. see https://learn.microsoft.com/windows/win32/debug/pe-format
+/// </summary>
+internal static class PEOffsets
+{
+ private const int PESignatureSize = sizeof(int);
+ private const int CoffHeaderSize = 20;
+ public const ushort DosImageSignature = 0x5A4D;
+ public const int PEHeaderSize = PESignatureSize + CoffHeaderSize;
+ public const int OneSectionHeaderSize = 40;
+ public const int DataDirectoryEntrySize = 8;
+
+ public const int ResourceTableDataDirectoryIndex = 2;
+
+ public static class DosStub
+ {
+ public const int PESignatureOffset = 0x3c;
+ }
+
+ /// offsets relative to Lfanew, which is pointer to first byte in header
+ public static class PEHeader
+ {
+ public const int NumberOfSections = PESignatureSize + 2;
+
+ private const int OptionalHeaderBase = PESignatureSize + CoffHeaderSize;
+ public const int InitializedDataSize = OptionalHeaderBase + 8;
+ public const int SizeOfImage = OptionalHeaderBase + 56;
+ public const int Subsystem = OptionalHeaderBase + 68;
+ public const int PE64DataDirectories = OptionalHeaderBase + 112;
+ public const int PE32DataDirectories = OptionalHeaderBase + 96;
+ }
+
+ /// offsets relative to each section header
+ public static class SectionHeader
+ {
+ public const int VirtualSize = 8;
+ public const int VirtualAddress = 12;
+ public const int RawSize = 16;
+ public const int RawPointer = 20;
+ public const int RelocationsPointer = 24;
+ public const int LineNumbersPointer = 28;
+ public const int NumberOfRelocations = 32;
+ public const int NumberOfLineNumbers = 34;
+ public const int SectionCharacteristics = 36;
+ }
+
+ public static class DataDirectoryEntry
+ {
+ public const int VirtualAddressOffset = 0;
+ public const int SizeOffset = 4;
+ }
+
+ public enum Subsystem : ushort
+ {
+ WindowsGui = 2,
+ WindowsCui = 3,
+ }
+}
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+using System.Linq;
+using System.Reflection.PortableExecutable;
+using Microsoft.NET.HostModel.Win32Resources;
namespace Microsoft.NET.HostModel
{
/// <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.
+ /// Provides methods for modifying the embedded native resources in a PE image.
/// </summary>
public class ResourceUpdater : IDisposable
{
- private sealed class Kernel32
- {
- //
- // Native methods for updating resources
- //
-
- [DllImport(nameof(Kernel32), CharSet = CharSet.Unicode, 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);
-
- // 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,
- string 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), CharSet = CharSet.Unicode, 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);
-
- public const int ERROR_CALL_NOT_IMPLEMENTED = 0x78;
- }
-
- /// <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 sealed class SafeUpdateHandle : SafeHandle
- {
- public 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;
+ private readonly FileStream stream;
+ private readonly PEReader _reader;
+ private ResourceData _resourceData;
+ private readonly bool leaveOpen;
///<summary>
/// Determines if the ResourceUpdater is supported by the current operating system.
/// </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
- // On Nano Server 20213, `BeginUpdateResource` fails with ERROR_CALL_NOT_IMPLEMENTED
- using (var handle = Kernel32.BeginUpdateResource("", false))
- {
- int lastWin32Error = Marshal.GetLastWin32Error();
-
- if (handle.IsInvalid && (lastWin32Error == 0 || lastWin32Error == Kernel32.ERROR_CALL_NOT_IMPLEMENTED))
- {
- 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.
+ /// Create a resource updater for the given PE file.
+ /// 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)
+ : this(new FileStream(peFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
- hUpdate = Kernel32.BeginUpdateResource(peFile, false);
- if (hUpdate.IsInvalid)
+ }
+
+ /// <summary>
+ /// Create a resource updater for the given PE file. This
+ /// 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(FileStream stream, bool leaveOpen = false)
+ {
+ this.stream = stream;
+ this.leaveOpen = leaveOpen;
+ try
+ {
+ this.stream.Seek(0, SeekOrigin.Begin);
+ _reader = new PEReader(this.stream, PEStreamOptions.LeaveOpen);
+ _resourceData = new ResourceData(_reader);
+ }
+ catch (Exception)
{
- ThrowExceptionForLastWin32Error();
+ if (!leaveOpen)
+ this.stream?.Dispose();
+ throw;
}
}
/// </summary>
public ResourceUpdater AddResourcesFromPEImage(string peFile)
{
- if (hUpdate.IsInvalid)
- {
+ if (_resourceData == null)
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();
- }
- }
+ using var module = new PEReader(File.Open(peFile, FileMode.Open, FileAccess.Read, FileShare.Read));
+ var moduleResources = new ResourceData(module);
+ _resourceData.CopyResourcesFrom(moduleResources);
return this;
}
return ((uint)lpType >> 16) == 0;
}
+ private const int LangID_LangNeutral_SublangNeutral = 0;
+
/// <summary>
/// Add a language-neutral integer resource from a byte[] with
/// a particular type and name. This will not modify the
/// </summary>
public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
{
- if (hUpdate.IsInvalid)
- {
- ThrowExceptionForInvalidUpdate();
- }
-
if (!IsIntResource(lpType) || !IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource types");
}
+ if (_resourceData == null)
+ ThrowExceptionForInvalidUpdate();
- if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
- {
- ThrowExceptionForLastWin32Error();
- }
+ _resourceData.AddResource((ushort)lpName, (ushort)lpType, LangID_LangNeutral_SublangNeutral, data);
return this;
}
/// </summary>
public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
{
- if (hUpdate.IsInvalid)
- {
- ThrowExceptionForInvalidUpdate();
- }
-
if (!IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource names");
}
+ if (_resourceData == null)
+ ThrowExceptionForInvalidUpdate();
- if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
- {
- ThrowExceptionForLastWin32Error();
- }
+ _resourceData.AddResource((ushort)lpName, lpType, LangID_LangNeutral_SublangNeutral, data);
return this;
}
/// </summary>
public void Update()
{
- if (hUpdate.IsInvalid)
- {
+ if (_resourceData == null)
ThrowExceptionForInvalidUpdate();
- }
- try
+ int resourceSectionIndex = _reader.PEHeaders.SectionHeaders.Length;
+ for (int i = 0; i < _reader.PEHeaders.SectionHeaders.Length; i++)
{
- if (!Kernel32.EndUpdateResource(hUpdate, false))
+ if (_reader.PEHeaders.SectionHeaders[i].Name == ".rsrc")
{
- ThrowExceptionForLastWin32Error();
+ resourceSectionIndex = i;
+ break;
}
}
- 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;
- }
+ int fileAlignment = _reader.PEHeaders.PEHeader!.FileAlignment;
+ int sectionAlignment = _reader.PEHeaders.PEHeader!.SectionAlignment;
- 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))
+ bool needsAddSection = resourceSectionIndex == _reader.PEHeaders.SectionHeaders.Length;
+ bool isRsrcIsLastSection;
+ int rsrcPointerToRawData;
+ int rsrcVirtualAddress;
+ int rsrcOriginalRawDataSize;
+ int rsrcOriginalVirtualSize;
+ if (needsAddSection)
{
- CaptureEnumResourcesErrorInfo(lParam);
- return false;
+ isRsrcIsLastSection = true;
+
+ SectionHeader lastSection = _reader.PEHeaders.SectionHeaders.Last();
+ rsrcPointerToRawData =
+ GetAligned(lastSection.PointerToRawData + lastSection.SizeOfRawData, fileAlignment);
+ rsrcVirtualAddress = GetAligned(lastSection.VirtualAddress + lastSection.VirtualSize, sectionAlignment);
+ rsrcOriginalRawDataSize = 0;
+ rsrcOriginalVirtualSize = 0;
}
- 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)
+ else
{
- CaptureEnumResourcesErrorInfo(lParam);
- return false;
- }
+ isRsrcIsLastSection = _reader.PEHeaders.SectionHeaders.Length - 1 == resourceSectionIndex;
- // 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;
+ SectionHeader resourceSection = _reader.PEHeaders.SectionHeaders[resourceSectionIndex];
+ rsrcPointerToRawData = resourceSection.PointerToRawData;
+ rsrcVirtualAddress = resourceSection.VirtualAddress;
+ rsrcOriginalRawDataSize = resourceSection.SizeOfRawData;
+ rsrcOriginalVirtualSize = resourceSection.VirtualSize;
}
- // 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;
- }
+ var objectDataBuilder = new ObjectDataBuilder();
+ _resourceData.WriteResources(rsrcVirtualAddress, ref objectDataBuilder);
+ var rsrcSectionData = objectDataBuilder.ToData();
- if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource)))
- {
- CaptureEnumResourcesErrorInfo(lParam);
- return false;
- }
+ int rsrcSectionDataSize = rsrcSectionData.Length;
+ int newSectionSize = GetAligned(rsrcSectionDataSize, fileAlignment);
+ int newSectionVirtualSize = GetAligned(rsrcSectionDataSize, sectionAlignment);
- return true;
- }
+ int delta = newSectionSize - GetAligned(rsrcOriginalRawDataSize, fileAlignment);
+ int virtualDelta = newSectionVirtualSize - GetAligned(rsrcOriginalVirtualSize, sectionAlignment);
- private sealed class EnumResourcesErrorInfo
- {
- public int hResult;
- public bool failedToLockResource;
+ int trailingSectionVirtualStart = rsrcVirtualAddress + rsrcOriginalVirtualSize;
+ int trailingSectionStart = rsrcPointerToRawData + rsrcOriginalRawDataSize;
+ int trailingSectionLength = (int)(stream.Length - trailingSectionStart);
- public void ThrowException()
+ bool needsMoveTrailingSections = !isRsrcIsLastSection && delta > 0;
+ long finalImageSize = trailingSectionStart + Math.Max(delta, 0) + trailingSectionLength;
+
+ using (var mmap = MemoryMappedFile.CreateFromFile(stream, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+ using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
{
- if (failedToLockResource)
+ int peSignatureOffset = ReadI32(accessor, PEOffsets.DosStub.PESignatureOffset);
+ int sectionBase = peSignatureOffset + PEOffsets.PEHeaderSize +
+ (ushort)_reader.PEHeaders.CoffHeader.SizeOfOptionalHeader;
+
+ if (needsAddSection)
+ {
+ int resourceSectionBase = sectionBase + PEOffsets.OneSectionHeaderSize * resourceSectionIndex;
+ // ensure we have space for new section header
+ if (resourceSectionBase + PEOffsets.OneSectionHeaderSize >
+ _reader.PEHeaders.SectionHeaders[0].PointerToRawData)
+ throw new InvalidOperationException("Cannot add section header");
+
+ WriteI32(accessor, peSignatureOffset + PEOffsets.PEHeader.NumberOfSections, resourceSectionIndex + 1);
+
+ // section name ".rsrc\0\0\0" = 2E 72 73 72 63 00 00 00
+ accessor.Write(resourceSectionBase + 0, (byte)0x2E);
+ accessor.Write(resourceSectionBase + 1, (byte)0x72);
+ accessor.Write(resourceSectionBase + 2, (byte)0x73);
+ accessor.Write(resourceSectionBase + 3, (byte)0x72);
+ accessor.Write(resourceSectionBase + 4, (byte)0x63);
+ accessor.Write(resourceSectionBase + 5, (byte)0x00);
+ accessor.Write(resourceSectionBase + 6, (byte)0x00);
+ accessor.Write(resourceSectionBase + 7, (byte)0x00);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.VirtualSize, rsrcSectionDataSize);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.VirtualAddress, rsrcVirtualAddress);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.RawSize, newSectionSize);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.RawPointer, rsrcPointerToRawData);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.RelocationsPointer, 0);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.LineNumbersPointer, 0);
+ WriteI16(accessor, resourceSectionBase + PEOffsets.SectionHeader.NumberOfRelocations, 0);
+ WriteI16(accessor, resourceSectionBase + PEOffsets.SectionHeader.NumberOfLineNumbers, 0);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.SectionCharacteristics,
+ (int)(SectionCharacteristics.ContainsInitializedData | SectionCharacteristics.MemRead));
+ }
+
+ if (needsMoveTrailingSections)
+ {
+ byte[] moveTrailingSectionBuffer = new byte[trailingSectionLength];
+ accessor.ReadArray(trailingSectionStart, moveTrailingSectionBuffer, 0, trailingSectionLength);
+ accessor.WriteArray(trailingSectionStart + delta, moveTrailingSectionBuffer, 0, trailingSectionLength);
+
+ for (int i = resourceSectionIndex + 1; i < _reader.PEHeaders.SectionHeaders.Length; i++)
+ {
+ int currentSectionBase = sectionBase + PEOffsets.OneSectionHeaderSize * i;
+
+ ModifyI32(accessor, currentSectionBase + PEOffsets.SectionHeader.VirtualAddress,
+ pointer => pointer + virtualDelta);
+ ModifyI32(accessor, currentSectionBase + PEOffsets.SectionHeader.RawPointer,
+ pointer => pointer + delta);
+ }
+ }
+
+ if (rsrcSectionDataSize != rsrcOriginalVirtualSize)
{
- Debug.Assert(hResult == 0);
- throw new ResourceNotAvailableException("Failed to lock resource");
+ // update size of .rsrc section
+ int resourceSectionBase = sectionBase + PEOffsets.OneSectionHeaderSize * resourceSectionIndex;
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.VirtualSize, rsrcSectionDataSize);
+ WriteI32(accessor, resourceSectionBase + PEOffsets.SectionHeader.RawSize, newSectionSize);
+
+ void PatchRVA(int offset)
+ {
+ ModifyI32(accessor, offset,
+ pointer => pointer >= trailingSectionVirtualStart ? pointer + virtualDelta : pointer);
+ }
+
+ int dataDirectoriesOffset = _reader.PEHeaders.PEHeader.Magic == PEMagic.PE32Plus
+ ? peSignatureOffset + PEOffsets.PEHeader.PE64DataDirectories
+ : peSignatureOffset + PEOffsets.PEHeader.PE32DataDirectories;
+
+ // fix header
+ ModifyI32(accessor, peSignatureOffset + PEOffsets.PEHeader.InitializedDataSize,
+ size => size + delta);
+ ModifyI32(accessor, peSignatureOffset + PEOffsets.PEHeader.SizeOfImage,
+ size => size + virtualDelta);
+
+ if (needsMoveTrailingSections)
+ {
+ // fix RVA in DataDirectory
+ for (int i = 0; i < _reader.PEHeaders.PEHeader.NumberOfRvaAndSizes; i++)
+ PatchRVA(dataDirectoriesOffset + i * PEOffsets.DataDirectoryEntrySize +
+ PEOffsets.DataDirectoryEntry.VirtualAddressOffset);
+ }
+
+ // update the ResourceTable in DataDirectories
+ int resourceTableOffset = dataDirectoriesOffset + PEOffsets.ResourceTableDataDirectoryIndex * PEOffsets.DataDirectoryEntrySize;
+ WriteI32(accessor, resourceTableOffset + PEOffsets.DataDirectoryEntry.VirtualAddressOffset, rsrcVirtualAddress);
+ WriteI32(accessor, resourceTableOffset + PEOffsets.DataDirectoryEntry.SizeOffset, rsrcSectionDataSize);
}
- Debug.Assert(hResult != 0);
- throw new HResultException(hResult);
+ accessor.WriteArray(rsrcPointerToRawData, rsrcSectionData, 0, rsrcSectionDataSize);
+
+ // clear rest
+ //Array.Fill is standard 2.1
+ for (int i = rsrcSectionDataSize; i < newSectionSize; i++)
+ accessor.Write(rsrcPointerToRawData + i, (byte)0);
+
+ _resourceData = null;
}
}
- private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr)
+ private static int ReadI32(MemoryMappedViewAccessor buffer, int position)
{
- int hResult = Marshal.GetHRForLastWin32Error();
- if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT)
- {
- GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr);
- var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target;
- errorInfo.hResult = hResult;
- }
+ return buffer.ReadByte(position + 0)
+ | (buffer.ReadByte(position + 1) << 8)
+ | (buffer.ReadByte(position + 2) << 16)
+ | (buffer.ReadByte(position + 3) << 24);
}
- private sealed class ResourceNotAvailableException : Exception
+ private static void WriteI32(MemoryMappedViewAccessor buffer, int position, int data)
{
- public ResourceNotAvailableException(string message) : base(message)
- {
- }
+ buffer.Write(position + 0, (byte)(data & 0xFF));
+ buffer.Write(position + 1, (byte)(data >> 8 & 0xFF));
+ buffer.Write(position + 2, (byte)(data >> 16 & 0xFF));
+ buffer.Write(position + 3, (byte)(data >> 24 & 0xFF));
}
-
- private static void ThrowExceptionForLastWin32Error()
+ private static void WriteI16(MemoryMappedViewAccessor buffer, int position, short data)
{
- throw new HResultException(Marshal.GetHRForLastWin32Error());
+ buffer.Write(position + 0, (byte)(data & 0xFF));
+ buffer.Write(position + 1, (byte)(data >> 8 & 0xFF));
}
+ private static void ModifyI32(MemoryMappedViewAccessor buffer, int position, Func<int, int> modifier) =>
+ WriteI32(buffer, position, modifier(ReadI32(buffer, position)));
+
+ public static int GetAligned(int integer, int alignWith) => (integer + alignWith - 1) & ~(alignWith - 1);
+
private static void ThrowExceptionForInvalidUpdate()
{
- throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates");
+ throw new InvalidOperationException(
+ "Update handle is invalid. This instance may not be used for further updates");
}
public void Dispose()
public void Dispose(bool disposing)
{
- if (disposing)
+ if (disposing && !leaveOpen)
{
- hUpdate.Dispose();
+ _reader.Dispose();
+ stream.Dispose();
}
}
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+using Debug = System.Diagnostics.Debug;
+
+namespace Microsoft.NET.HostModel.Win32Resources
+{
+ public struct ObjectDataBuilder
+ {
+ public ObjectDataBuilder()
+ {
+ _data = default(ArrayBuilder<byte>);
+#if DEBUG
+ _numReservations = 0;
+#endif
+ }
+
+ private ArrayBuilder<byte> _data;
+
+#if DEBUG
+ private int _numReservations;
+#endif
+
+ public int CountBytes
+ {
+ get
+ {
+ return _data.Count;
+ }
+ }
+
+ public void EmitByte(byte emit)
+ {
+ _data.Add(emit);
+ }
+
+ public void EmitShort(short emit)
+ {
+ EmitByte((byte)(emit & 0xFF));
+ EmitByte((byte)((emit >> 8) & 0xFF));
+ }
+
+ public void EmitUShort(ushort emit)
+ {
+ EmitByte((byte)(emit & 0xFF));
+ EmitByte((byte)((emit >> 8) & 0xFF));
+ }
+
+ public void EmitInt(int emit)
+ {
+ EmitByte((byte)(emit & 0xFF));
+ EmitByte((byte)((emit >> 8) & 0xFF));
+ EmitByte((byte)((emit >> 16) & 0xFF));
+ EmitByte((byte)((emit >> 24) & 0xFF));
+ }
+
+ public void EmitUInt(uint emit)
+ {
+ EmitByte((byte)(emit & 0xFF));
+ EmitByte((byte)((emit >> 8) & 0xFF));
+ EmitByte((byte)((emit >> 16) & 0xFF));
+ EmitByte((byte)((emit >> 24) & 0xFF));
+ }
+
+ public void EmitLong(long emit)
+ {
+ EmitByte((byte)(emit & 0xFF));
+ EmitByte((byte)((emit >> 8) & 0xFF));
+ EmitByte((byte)((emit >> 16) & 0xFF));
+ EmitByte((byte)((emit >> 24) & 0xFF));
+ EmitByte((byte)((emit >> 32) & 0xFF));
+ EmitByte((byte)((emit >> 40) & 0xFF));
+ EmitByte((byte)((emit >> 48) & 0xFF));
+ EmitByte((byte)((emit >> 56) & 0xFF));
+ }
+
+ public void EmitBytes(byte[] bytes)
+ {
+ _data.Append(bytes);
+ }
+
+ public void EmitBytes(byte[] bytes, int offset, int length)
+ {
+ _data.Append(bytes, offset, length);
+ }
+
+ internal void EmitBytes(ArrayBuilder<byte> bytes)
+ {
+ _data.Append(bytes);
+ }
+
+ public void EmitZeros(int numBytes)
+ {
+ _data.ZeroExtend(numBytes);
+ }
+
+ private Reservation GetReservationTicket(int size)
+ {
+#if DEBUG
+ _numReservations++;
+#endif
+ Reservation ticket = (Reservation)_data.Count;
+ _data.ZeroExtend(size);
+ return ticket;
+ }
+
+#pragma warning disable CA1822 // Mark members as static
+ private int ReturnReservationTicket(Reservation reservation)
+#pragma warning restore CA1822 // Mark members as static
+ {
+#if DEBUG
+ Debug.Assert(_numReservations > 0);
+ _numReservations--;
+#endif
+ return (int)reservation;
+ }
+
+ public Reservation ReserveByte()
+ {
+ return GetReservationTicket(1);
+ }
+
+ public void EmitByte(Reservation reservation, byte emit)
+ {
+ int offset = ReturnReservationTicket(reservation);
+ _data[offset] = emit;
+ }
+
+ public Reservation ReserveShort()
+ {
+ return GetReservationTicket(2);
+ }
+
+ public void EmitShort(Reservation reservation, short emit)
+ {
+ int offset = ReturnReservationTicket(reservation);
+ _data[offset] = (byte)(emit & 0xFF);
+ _data[offset + 1] = (byte)((emit >> 8) & 0xFF);
+ }
+
+ public Reservation ReserveInt()
+ {
+ return GetReservationTicket(4);
+ }
+
+ public void EmitInt(Reservation reservation, int emit)
+ {
+ int offset = ReturnReservationTicket(reservation);
+ _data[offset] = (byte)(emit & 0xFF);
+ _data[offset + 1] = (byte)((emit >> 8) & 0xFF);
+ _data[offset + 2] = (byte)((emit >> 16) & 0xFF);
+ _data[offset + 3] = (byte)((emit >> 24) & 0xFF);
+ }
+
+ public void EmitUInt(Reservation reservation, uint emit)
+ {
+ int offset = ReturnReservationTicket(reservation);
+ _data[offset] = (byte)(emit & 0xFF);
+ _data[offset + 1] = (byte)((emit >> 8) & 0xFF);
+ _data[offset + 2] = (byte)((emit >> 16) & 0xFF);
+ _data[offset + 3] = (byte)((emit >> 24) & 0xFF);
+ }
+
+ public byte[] ToData()
+ {
+#if DEBUG
+ Debug.Assert(_numReservations == 0);
+#endif
+
+ return _data.ToArray();
+ }
+
+ public enum Reservation { }
+
+ public void PadAlignment(int align)
+ {
+ Debug.Assert((align == 2) || (align == 4) || (align == 8) || (align == 16));
+ int misalignment = _data.Count & (align - 1);
+ if (misalignment != 0)
+ {
+ EmitZeros(align - misalignment);
+ }
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <OutputType>Exe</OutputType>
+ <RuntimeIdentifier>$(TestTargetRid)</RuntimeIdentifier>
+ <RuntimeFrameworkVersion>$(MNAVersion)</RuntimeFrameworkVersion>
+ <Win32Resource>test.res</Win32Resource>
+ <!-- Current .NET SDK can't run CreateAppHost so disable creating AppHost on build -->
+ <UseAppHost>false</UseAppHost>
+ </PropertyGroup>
+
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+
+namespace AppWithUnknownLanguageResource
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ Console.WriteLine("Hello World!");
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+LANGUAGE 0x041B, 0
+funny RCDATA { 1L }
+10 RCDATA { 3L }
+LANGUAGE 0x0405, 0
+funny RCDATA { 2L }
+10 RCDATA { 4L }
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.DotNet.CoreSetup.Test;
+using Xunit;
+
+namespace Microsoft.NET.HostModel.Tests
+{
+ // https://github.com/dotnet/runtime/issues/88465
+ public class AppWithUnknownLanguageResource : IClassFixture<AppWithUnknownLanguageResource.SharedTestState>
+ {
+ private SharedTestState sharedTestState;
+
+ public AppWithUnknownLanguageResource(SharedTestState fixture)
+ {
+ sharedTestState = fixture;
+ }
+
+ [Fact]
+ private void Can_Build_App_With_Resource_With_Unknown_Language()
+ {
+ var fixture = sharedTestState.TestFixture.Copy();
+
+ fixture.TestProject.BuiltApp.CreateAppHost();
+ }
+
+ public class SharedTestState : IDisposable
+ {
+ public RepoDirectoriesProvider RepoDirectories { get; set; }
+ public TestProjectFixture TestFixture { get; set; }
+
+ public SharedTestState()
+ {
+ RepoDirectories = new RepoDirectoriesProvider();
+ var testFixture = new TestProjectFixture("AppWithUnknownLanguageResource", RepoDirectories);
+ testFixture.EnsureRestored().BuildProject();
+ TestFixture = testFixture;
+ }
+
+ public void Dispose()
+ {
+ TestFixture.Dispose();
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+using Microsoft.NET.HostModel;
+using Microsoft.NET.HostModel.Win32Resources;
+using Xunit;
+
+namespace Microsoft.NET.HostModel.Tests;
+
+public class ResourceUpdaterTests
+{
+ class TempFile : IDisposable
+ {
+ public readonly FileStream Stream;
+ private readonly string _path;
+
+ public TempFile()
+ {
+ _path = Path.GetTempFileName();
+ Stream = new FileStream(_path, FileMode.Open);
+ }
+
+ public void Dispose()
+ {
+ Stream.Close();
+ File.Delete(_path);
+ }
+ }
+
+ private TempFile CreateTestPEFileWithoutRsrc()
+ {
+ var peBuilder = new ManagedPEBuilder(
+ PEHeaderBuilder.CreateExecutableHeader(),
+ new MetadataRootBuilder(new MetadataBuilder()),
+ ilStream: new BlobBuilder());
+ var peImageBuilder = new BlobBuilder();
+ peBuilder.Serialize(peImageBuilder);
+ var tempFile = new TempFile();
+ tempFile.Stream.Write(peImageBuilder.ToArray());
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ return tempFile;
+ }
+
+ private TempFile GetCurrentAssemblyMemoryStream()
+ {
+ var tempFile = new TempFile();
+ tempFile.Stream.Write(File.ReadAllBytes(Assembly.GetExecutingAssembly().Location));
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ return tempFile;
+ }
+
+ [Fact]
+ void AddResource_AddToPEWithoutRsrc()
+ {
+ using var tempFile = CreateTestPEFileWithoutRsrc();
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResource("Test Resource"u8.ToArray(), "testType", 0);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var reader = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ {
+ var resourceReader = new ResourceData(reader);
+ byte[]? testType = resourceReader.FindResource(0, "testType", 0);
+ Assert.Equal("Test Resource"u8.ToArray(), testType);
+ }
+ }
+
+ [Fact]
+ void AddResource_AddToExistingRsrc()
+ {
+ using var tempFile = GetCurrentAssemblyMemoryStream();
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResource("OtherResource"u8.ToArray(), "testType2", 0);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var reader = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ {
+ var resourceReader = new ResourceData(reader);
+ byte[]? testType = resourceReader.FindResource(0, "testType2", 0);
+ Assert.Equal("OtherResource"u8.ToArray(), testType);
+ }
+ }
+
+ [Fact]
+ void AddResource_AddResourceWithIdType()
+ {
+ using var tempFile = GetCurrentAssemblyMemoryStream();
+ const ushort IdTestType = 100;
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResource("OtherResource"u8.ToArray(), IdTestType, 0);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var reader = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ {
+ var resourceReader = new ResourceData(reader);
+ byte[]? testType = resourceReader.FindResource(0, IdTestType, 0);
+ Assert.Equal("OtherResource"u8.ToArray(), testType);
+ }
+ }
+
+ [Fact]
+ void AddResource_AddTwoSameStringTypeWithDifferName()
+ {
+ using var tempFile = GetCurrentAssemblyMemoryStream();
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResource("Test Resource"u8.ToArray(), "testType", 0);
+ updater.AddResource("Other Resource"u8.ToArray(), "testType", 1);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var reader = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ {
+ var resourceReader = new ResourceData(reader);
+ byte[]? name0 = resourceReader.FindResource(0, "testType", 0);
+ byte[]? name1 = resourceReader.FindResource(1, "testType", 0);
+ Assert.Equal("Test Resource"u8.ToArray(), name0);
+ Assert.Equal("Other Resource"u8.ToArray(), name1);
+ }
+ }
+
+ [Fact]
+ void AddResource_AddTwoSameUShortTypeWithDifferName()
+ {
+ using var tempFile = GetCurrentAssemblyMemoryStream();
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResource("Test Resource"u8.ToArray(), 11, 0);
+ updater.AddResource("Other Resource"u8.ToArray(), 11, 1);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var reader = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ {
+ var resourceReader = new ResourceData(reader);
+ byte[]? name0 = resourceReader.FindResource(0, 11, 0);
+ byte[]? name1 = resourceReader.FindResource(1, 11, 0);
+ Assert.Equal("Test Resource"u8.ToArray(), name0);
+ Assert.Equal("Other Resource"u8.ToArray(), name1);
+ }
+ }
+
+ [Fact]
+ void AddResourcesFromPEImage()
+ {
+ using var tempFile = CreateTestPEFileWithoutRsrc();
+
+ using (var updater = new ResourceUpdater(tempFile.Stream, true))
+ {
+ updater.AddResourcesFromPEImage(Assembly.GetExecutingAssembly().Location);
+ updater.Update();
+ }
+
+ tempFile.Stream.Seek(0, SeekOrigin.Begin);
+
+ using (var modified = new PEReader(tempFile.Stream, PEStreamOptions.LeaveOpen))
+ using (var assembly = new PEReader(File.Open(Assembly.GetExecutingAssembly().Location, FileMode.Open, FileAccess.Read, FileShare.Read)))
+ {
+ var modifiedReader = new ResourceData(modified);
+ var assemblyReader = new ResourceData(assembly);
+ foreach ((object nameObj, object typeObj, ushort language, byte[] data) in assemblyReader.GetAllResources())
+ {
+ byte[]? found;
+ switch (nameObj, typeObj)
+ {
+ case (ushort name, ushort type):
+ found = modifiedReader.FindResource(name, type, language);
+ break;
+ case (ushort name, string type):
+ found = modifiedReader.FindResource(name, type, language);
+ break;
+ case (string name, ushort type):
+ found = modifiedReader.FindResource(name, type, language);
+ break;
+ case (string name, string type):
+ found = modifiedReader.FindResource(name, type, language);
+ break;
+ default:
+ found = null;
+ Assert.Fail("name or type is not string nor ushort.");
+ break;
+ }
+ Assert.Equal(data, found);
+ }
+ }
+ }
+}