Crossplatform ResourceUpdater (#89303)
authoranatawa12 <anatawa12@icloud.com>
Tue, 8 Aug 2023 16:18:54 +0000 (01:18 +0900)
committerGitHub <noreply@github.com>
Tue, 8 Aug 2023 16:18:54 +0000 (09:18 -0700)
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.

18 files changed:
src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs
src/coreclr/tools/Common/Compiler/Win32Resources/ResourceData.Reader.cs
src/coreclr/tools/Common/Compiler/Win32Resources/ResourceData.ResourcesDataModel.cs
src/coreclr/tools/Common/Compiler/Win32Resources/ResourceData.UpdateResourceDataModel.cs
src/coreclr/tools/Common/Compiler/Win32Resources/ResourceData.Win32Structs.cs
src/coreclr/tools/Common/Compiler/Win32Resources/ResourceData.cs
src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
src/installer/managed/Microsoft.NET.HostModel/AppHost/PEUtils.cs
src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
src/installer/managed/Microsoft.NET.HostModel/PEOffsets.cs [new file with mode: 0644]
src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
src/installer/managed/Microsoft.NET.HostModel/Win32Resources/ObjectDataBuilder.cs [new file with mode: 0644]
src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/AppWithUnknownLanguageResource.csproj [new file with mode: 0644]
src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/Program.cs [new file with mode: 0644]
src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.rc [new file with mode: 0644]
src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.res [new file with mode: 0644]
src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppWithUnknownLanguageResource.cs [new file with mode: 0644]
src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/ResourceUpdaterTests.cs [new file with mode: 0644]

index 67c9931..d9d56e3 100644 (file)
@@ -9,6 +9,8 @@ using Debug = System.Diagnostics.Debug;
 
 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
index d95eae8..600f906 100644 (file)
@@ -5,7 +5,11 @@ using System;
 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
     {
@@ -51,7 +55,7 @@ namespace ILCompiler.Win32Resources
                             if (!resourceFilter(typeName, name, (ushort)languageName))
                                 return;
                         }
-                        AddResource(typeName, name, (ushort)languageName, data);
+                        AddResourceInternal(name, typeName, (ushort)languageName, data);
                     }
                 }
             }
index 3a2fdc1..d455409 100644 (file)
@@ -4,7 +4,11 @@
 using System;
 using System.Collections.Generic;
 
+#if HOST_MODEL
+namespace Microsoft.NET.HostModel.Win32Resources
+#else
 namespace ILCompiler.Win32Resources
+#endif
 {
     public unsafe partial class ResourceData
     {
index d0e1bac..8ee6aae 100644 (file)
@@ -4,11 +4,15 @@
 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;
 
index 2045a0b..b13382d 100644 (file)
@@ -5,9 +5,15 @@ using System.Collections.Generic;
 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
     {
@@ -81,9 +87,15 @@ namespace ILCompiler.Win32Resources
                 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,
@@ -91,6 +103,7 @@ namespace ILCompiler.Win32Resources
                     RelocType.IMAGE_REL_BASED_ABSOLUTE,
 #endif
                     offsetFromSymbol);
+#endif
                 dataBuilder.EmitInt(sizeOfData);
                 dataBuilder.EmitInt(1252);  // CODEPAGE = DEFAULT_CODEPAGE
                 dataBuilder.EmitInt(0); // RESERVED
index c8d42fa..ff6b7fb 100644 (file)
@@ -4,13 +4,19 @@
 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
@@ -18,11 +24,26 @@ namespace ILCompiler.Win32Resources
     /// </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;
@@ -34,6 +55,7 @@ namespace ILCompiler.Win32Resources
                 ReadResourceData(resourceDataBlob, peFile, resourceFilter);
             }
         }
+#endif
 
         /// <summary>
         /// Find a resource in the resource data
@@ -67,6 +89,44 @@ namespace ILCompiler.Win32Resources
             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
@@ -81,12 +141,32 @@ namespace ILCompiler.Win32Resources
             }
         }
 
+        /// <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);
 
@@ -159,7 +239,11 @@ namespace ILCompiler.Win32Resources
             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
         }
index d9d3deb..876e9bf 100644 (file)
@@ -66,24 +66,6 @@ namespace Microsoft.NET.HostModel.AppHost
                 }
             }
 
-            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(() =>
@@ -115,6 +97,13 @@ namespace Microsoft.NET.HostModel.AppHost
                             {
                                 MachOUtils.RemoveSignature(fileStream);
                             }
+
+                            if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
+                            {
+                                using var updater = new ResourceUpdater(fileStream, true);
+                                updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
+                                updater.Update();
+                            }
                         }
                     }
                     finally
@@ -125,8 +114,6 @@ namespace Microsoft.NET.HostModel.AppHost
                     }
                 });
 
-                RetryUtil.RetryOnWin32Error(UpdateResources);
-
                 if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                 {
                     var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
index e38a5c6..0d0b33e 100644 (file)
@@ -1,7 +1,6 @@
 // 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;
 
@@ -10,31 +9,6 @@ namespace Microsoft.NET.HostModel.AppHost
     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>
@@ -50,7 +24,8 @@ namespace Microsoft.NET.HostModel.AppHost
 
                 // 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;
                 }
@@ -69,13 +44,13 @@ namespace Microsoft.NET.HostModel.AppHost
         {
             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;
             }
         }
 
@@ -93,24 +68,24 @@ namespace Microsoft.NET.HostModel.AppHost
                 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
             {
@@ -146,14 +121,14 @@ namespace Microsoft.NET.HostModel.AppHost
                 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];
             }
index 679289b..de2e9ac 100644 (file)
@@ -16,6 +16,7 @@
     <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>
 
 
diff --git a/src/installer/managed/Microsoft.NET.HostModel/PEOffsets.cs b/src/installer/managed/Microsoft.NET.HostModel/PEOffsets.cs
new file mode 100644 (file)
index 0000000..f09c41d
--- /dev/null
@@ -0,0 +1,63 @@
+// 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,
+    }
+}
index 879d390..dfbbb52 100644 (file)
 // 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.
@@ -186,50 +26,41 @@ namespace Microsoft.NET.HostModel
         /// </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;
             }
         }
 
@@ -242,48 +73,12 @@ namespace Microsoft.NET.HostModel
         /// </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;
         }
 
@@ -292,6 +87,8 @@ namespace Microsoft.NET.HostModel
             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
@@ -300,20 +97,14 @@ namespace Microsoft.NET.HostModel
         /// </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;
         }
@@ -326,20 +117,14 @@ namespace Microsoft.NET.HostModel
         /// </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;
         }
@@ -352,126 +137,201 @@ namespace Microsoft.NET.HostModel
         /// </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()
@@ -482,9 +342,10 @@ namespace Microsoft.NET.HostModel
 
         public void Dispose(bool disposing)
         {
-            if (disposing)
+            if (disposing && !leaveOpen)
             {
-                hUpdate.Dispose();
+                _reader.Dispose();
+                stream.Dispose();
             }
         }
     }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Win32Resources/ObjectDataBuilder.cs b/src/installer/managed/Microsoft.NET.HostModel/Win32Resources/ObjectDataBuilder.cs
new file mode 100644 (file)
index 0000000..be6712e
--- /dev/null
@@ -0,0 +1,187 @@
+// 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);
+            }
+        }
+    }
+}
diff --git a/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/AppWithUnknownLanguageResource.csproj b/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/AppWithUnknownLanguageResource.csproj
new file mode 100644 (file)
index 0000000..a1927cd
--- /dev/null
@@ -0,0 +1,13 @@
+<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>
diff --git a/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/Program.cs b/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/Program.cs
new file mode 100644 (file)
index 0000000..382d277
--- /dev/null
@@ -0,0 +1,18 @@
+// 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!");
+        }
+    }
+}
diff --git a/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.rc b/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.rc
new file mode 100644 (file)
index 0000000..7e3e071
--- /dev/null
@@ -0,0 +1,9 @@
+// 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 }
diff --git a/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.res b/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.res
new file mode 100644 (file)
index 0000000..4844d68
Binary files /dev/null and b/src/installer/tests/Assets/TestProjects/AppWithUnknownLanguageResource/test.res differ
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppWithUnknownLanguageResource.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppWithUnknownLanguageResource.cs
new file mode 100644 (file)
index 0000000..8e57c2a
--- /dev/null
@@ -0,0 +1,47 @@
+// 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();
+            }
+        }
+    }
+}
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/ResourceUpdaterTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/ResourceUpdaterTests.cs
new file mode 100644 (file)
index 0000000..6aefb0f
--- /dev/null
@@ -0,0 +1,216 @@
+// 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);
+            }
+        }
+    }
+}