Remove bundled HostModel dll and include runtime code

This commit is contained in:
Caelan Sayler
2021-12-12 13:56:53 +00:00
parent 6d4fe5a8e0
commit 6a7bc1ec38
54 changed files with 6170 additions and 6 deletions

View File

@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// An instance of this exception is thrown when an AppHost binary update
/// fails due to known user errors.
/// </summary>
public class AppHostUpdateException : Exception
{
internal AppHostUpdateException(string message = null)
: base(message)
{
}
}
/// <summary>
/// The application host executable cannot be customized because adding resources requires
/// that the build be performed on Windows (excluding Nano Server).
/// </summary>
public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
{
internal AppHostCustomizationUnsupportedOSException()
{
}
}
/// <summary>
/// The MachO application host executable cannot be customized because
/// it was not in the expected format
/// </summary>
public sealed class AppHostMachOFormatException : AppHostUpdateException
{
public readonly MachOFormatError Error;
internal AppHostMachOFormatException(MachOFormatError error)
{
Error = error;
}
}
/// <summary>
/// Unable to use the input file as application host executable because it's not a
/// Windows executable for the CUI (Console) subsystem.
/// </summary>
public sealed class AppHostNotCUIException : AppHostUpdateException
{
internal AppHostNotCUIException()
{
}
}
/// <summary>
/// Unable to use the input file as an application host executable
/// because it's not a Windows PE file
/// </summary>
public sealed class AppHostNotPEFileException : AppHostUpdateException
{
internal AppHostNotPEFileException()
{
}
}
/// <summary>
/// Unable to sign the apphost binary.
/// </summary>
public sealed class AppHostSigningException : AppHostUpdateException
{
public readonly int ExitCode;
internal AppHostSigningException(int exitCode, string signingErrorMessage)
: base(signingErrorMessage)
{
}
}
/// <summary>
/// Given app file name is longer than 1024 bytes
/// </summary>
public sealed class AppNameTooLongException : AppHostUpdateException
{
public string LongName { get; }
internal AppNameTooLongException(string name)
{
LongName = name;
}
}
}

View File

@@ -0,0 +1,202 @@
// 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;
namespace Microsoft.NET.HostModel.AppHost
{
public static class BinaryUtils
{
internal static unsafe void SearchAndReplace(
MemoryMappedViewAccessor accessor,
byte[] searchPattern,
byte[] patternToReplace,
bool pad0s = true)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
int position = KMPSearch(searchPattern, bytes, accessor.Capacity);
if (position < 0)
{
throw new PlaceHolderNotFoundInAppHostException(searchPattern);
}
accessor.WriteArray(
position: position,
array: patternToReplace,
offset: 0,
count: patternToReplace.Length);
if (pad0s)
{
Pad0(searchPattern, patternToReplace, bytes, position);
}
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset)
{
if (patternToReplace.Length < searchPattern.Length)
{
for (int i = patternToReplace.Length; i < searchPattern.Length; i++)
{
bytes[i + offset] = 0x0;
}
}
}
public static unsafe void SearchAndReplace(
string filePath,
byte[] searchPattern,
byte[] patternToReplace,
bool pad0s = true)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s);
}
}
}
internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern)
{
var safeBuffer = accessor.SafeMemoryMappedViewHandle;
return KMPSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (int)safeBuffer.ByteLength);
}
public static unsafe int SearchInFile(string filePath, byte[] searchPattern)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
{
return SearchInFile(accessor, searchPattern);
}
}
}
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
private static int[] ComputeKMPFailureFunction(byte[] pattern)
{
int[] table = new int[pattern.Length];
if (pattern.Length >= 1)
{
table[0] = -1;
}
if (pattern.Length >= 2)
{
table[1] = 0;
}
int pos = 2;
int cnd = 0;
while (pos < pattern.Length)
{
if (pattern[pos - 1] == pattern[cnd])
{
table[pos] = cnd + 1;
cnd++;
pos++;
}
else if (cnd > 0)
{
cnd = table[cnd];
}
else
{
table[pos] = 0;
pos++;
}
}
return table;
}
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength)
{
int m = 0;
int i = 0;
int[] table = ComputeKMPFailureFunction(pattern);
while (m + i < bytesLength)
{
if (pattern[i] == bytes[m + i])
{
if (i == pattern.Length - 1)
{
return m;
}
i++;
}
else
{
if (table[i] > -1)
{
m = m + i - table[i];
i = table[i];
}
else
{
m++;
i = 0;
}
}
}
return -1;
}
public static void CopyFile(string sourcePath, string destinationPath)
{
var destinationDirectory = new FileInfo(destinationPath).Directory.FullName;
if (!Directory.Exists(destinationDirectory))
{
Directory.CreateDirectory(destinationDirectory);
}
// Copy file to destination path so it inherits the same attributes/permissions.
File.Copy(sourcePath, destinationPath, overwrite: true);
}
internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length)
{
int pos = 0;
int bufSize = 16384; //16K
byte[] buf = new byte[bufSize];
length = Math.Min(length, sourceViewAccessor.Capacity);
do
{
int bytesRequested = Math.Min((int)length - pos, bufSize);
if (bytesRequested <= 0)
{
break;
}
int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested);
if (bytesRead > 0)
{
fileStream.Write(buf, 0, bytesRead);
pos += bytesRead;
}
}
while (true);
}
}
}

View File

@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
namespace Microsoft.NET.HostModel.AppHost
{
internal static class ElfUtils
{
// The Linux Headers are copied from elf.h
#pragma warning disable 0649
private struct ElfHeader
{
private byte EI_MAG0;
private byte EI_MAG1;
private byte EI_MAG2;
private byte EI_MAG3;
public bool IsValid()
{
return EI_MAG0 == 0x7f &&
EI_MAG1 == 0x45 &&
EI_MAG2 == 0x4C &&
EI_MAG3 == 0x46;
}
}
#pragma warning restore 0649
public static bool IsElfImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < 16) // EI_NIDENT = 16
{
return false;
}
byte[] eIdent = reader.ReadBytes(4);
// Check that the first four bytes are 0x7f, 'E', 'L', 'F'
return eIdent[0] == 0x7f &&
eIdent[1] == 0x45 &&
eIdent[2] == 0x4C &&
eIdent[3] == 0x46;
}
}
}
}

View File

@@ -0,0 +1,22 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
namespace Microsoft.NET.HostModel
{
/// <summary>
/// Represents an exception thrown because of a Win32 error
/// </summary>
public class HResultException : Exception
{
public readonly int Win32HResult;
public HResultException(int hResult) : base(hResult.ToString("X4"))
{
Win32HResult = hResult;
}
}
}

View File

@@ -0,0 +1,267 @@
// 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// Embeds the App Name into the AppHost.exe
/// If an apphost is a single-file bundle, updates the location of the bundle headers.
/// </summary>
public static class HostWriter
{
/// <summary>
/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
/// </summary>
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
/// <summary>
/// Create an AppHost with embedded configuration of app binary location
/// </summary>
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
public static void CreateAppHost(
string appHostSourceFilePath,
string appHostDestinationFilePath,
string appBinaryFilePath,
bool windowsGraphicalUserInterface = false,
string assemblyToCopyResorcesFrom = null,
bool enableMacOSCodeSign = false)
{
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
if (bytesToWrite.Length > 1024)
{
throw new AppNameTooLongException(appBinaryFilePath);
}
bool appHostIsPEImage = false;
void RewriteAppHost(MemoryMappedViewAccessor accessor)
{
// Re-write the destination apphost with the proper contents.
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
appHostIsPEImage = PEUtils.IsPEImage(accessor);
if (windowsGraphicalUserInterface)
{
if (!appHostIsPEImage)
{
throw new AppHostNotPEFileException();
}
PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
}
}
void UpdateResources()
{
if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
{
if (ResourceUpdater.IsSupportedOS())
{
// Copy resources from managed dll to the apphost
new ResourceUpdater(appHostDestinationFilePath)
.AddResourcesFromPEImage(assemblyToCopyResorcesFrom)
.Update();
}
else
{
throw new AppHostCustomizationUnsupportedOSException();
}
}
}
try
{
RetryUtil.RetryOnIOError(() =>
{
FileStream appHostSourceStream = null;
MemoryMappedFile memoryMappedFile = null;
MemoryMappedViewAccessor memoryMappedViewAccessor = null;
try
{
// Open the source host file.
appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
// Get the size of the source app host to ensure that we don't write extra data to the destination.
// On Windows, the size of the view accessor is rounded up to the next page boundary.
long sourceAppHostLength = appHostSourceStream.Length;
// Transform the host file in-memory.
RewriteAppHost(memoryMappedViewAccessor);
// Save the transformed host.
using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
{
BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength);
// Remove the signature from MachO hosts.
if (!appHostIsPEImage)
{
MachOUtils.RemoveSignature(fileStream);
}
}
}
finally
{
memoryMappedViewAccessor?.Dispose();
memoryMappedFile?.Dispose();
appHostSourceStream?.Dispose();
}
});
RetryUtil.RetryOnWin32Error(UpdateResources);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
const int EINTR = 4;
int chmodReturnCode = 0;
do
{
chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal);
}
while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
if (chmodReturnCode == -1)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}.");
}
if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
CodeSign(appHostDestinationFilePath);
}
}
catch (Exception ex)
{
// Delete the destination file so we don't leave an unmodified apphost
try
{
File.Delete(appHostDestinationFilePath);
}
catch (Exception failedToDeleteEx)
{
throw new AggregateException(ex, failedToDeleteEx);
}
throw;
}
}
/// <summary>
/// Set the current AppHost as a single-file bundle.
/// </summary>
/// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
/// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
public static void SetAsBundle(
string appHostPath,
long bundleHeaderOffset)
{
byte[] bundleHeaderPlaceholder = {
// 8 bytes represent the bundle header-offset
// Zero for non-bundle apphosts (default).
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
// Re-write the destination apphost with the proper contents.
RetryUtil.RetryOnIOError(() =>
BinaryUtils.SearchAndReplace(appHostPath,
bundleHeaderPlaceholder,
BitConverter.GetBytes(bundleHeaderOffset),
pad0s: false));
RetryUtil.RetryOnIOError(() =>
MachOUtils.AdjustHeadersForBundle(appHostPath));
// Memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
}
/// <summary>
/// Check if the an AppHost is a single-file bundle
/// </summary>
/// <param name="appHostFilePath">The path of Apphost to check</param>
/// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
/// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
{
byte[] bundleSignature = {
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
long headerOffset = 0;
void FindBundleHeader()
{
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath))
{
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
{
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
if (position == -1)
{
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
}
headerOffset = accessor.ReadInt64(position - sizeof(long));
}
}
}
RetryUtil.RetryOnIOError(FindBundleHeader);
bundleHeaderOffset = headerOffset;
return headerOffset != 0;
}
private static void CodeSign(string appHostPath)
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
const string codesign = @"/usr/bin/codesign";
if (!File.Exists(codesign))
return;
var psi = new ProcessStartInfo()
{
Arguments = $"-s - \"{appHostPath}\"",
FileName = codesign,
RedirectStandardError = true,
};
using (var p = Process.Start(psi))
{
p.WaitForExit();
if (p.ExitCode != 0)
throw new AppHostSigningException(p.ExitCode, p.StandardError.ReadToEnd());
}
}
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string pathname, int mode);
}
}

View File

@@ -0,0 +1,26 @@
// 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.AppHost
{
/// <summary>
/// Additional details about the failure with caused an AppHostMachOFormatException
/// </summary>
public enum MachOFormatError
{
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
SignBlobNotLast, // Signature blob must be at the very end of the file
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
MemoryMapAccessFault, // Error reading the memory-mapped apphost
InvalidUTF8, // UTF8 decoding failed
SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle)
}
}

View File

@@ -0,0 +1,446 @@
// 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;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.NET.HostModel.AppHost
{
internal static class MachOUtils
{
// The MachO Headers are copied from
// https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
//
// The data fields and enumerations match the structure definitions in the above file,
// and hence do not conform to C# CoreFx naming style.
private enum Magic : uint
{
MH_MAGIC = 0xfeedface,
MH_CIGAM = 0xcefaedfe,
MH_MAGIC_64 = 0xfeedfacf,
MH_CIGAM_64 = 0xcffaedfe
}
private enum FileType : uint
{
MH_EXECUTE = 0x2
}
#pragma warning disable 0649
private struct MachHeader
{
public Magic magic;
public int cputype;
public int cpusubtype;
public FileType filetype;
public uint ncmds;
public uint sizeofcmds;
public uint flags;
public uint reserved;
public bool Is64BitExecutable()
{
return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
}
public bool IsValid()
{
switch (magic)
{
case Magic.MH_CIGAM:
case Magic.MH_CIGAM_64:
case Magic.MH_MAGIC:
case Magic.MH_MAGIC_64:
return true;
default:
return false;
}
}
}
private enum Command : uint
{
LC_SYMTAB = 0x2,
LC_SEGMENT_64 = 0x19,
LC_CODE_SIGNATURE = 0x1d,
}
private struct LoadCommand
{
public Command cmd;
public uint cmdsize;
}
// The linkedit_data_command contains the offsets and sizes of a blob
// of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
private struct LinkEditDataCommand
{
public Command cmd;
public uint cmdsize;
public uint dataoff;
public uint datasize;
}
private struct SymtabCommand
{
public uint cmd;
public uint cmdsize;
public uint symoff;
public uint nsyms;
public uint stroff;
public uint strsize;
};
private unsafe struct SegmentCommand64
{
public Command cmd;
public uint cmdsize;
public fixed byte segname[16];
public ulong vmaddr;
public ulong vmsize;
public ulong fileoff;
public ulong filesize;
public int maxprot;
public int initprot;
public uint nsects;
public uint flags;
public string SegName
{
get
{
fixed (byte* p = segname)
{
int len = 0;
while (*(p + len) != 0 && len++ < 16) ;
try
{
return Encoding.UTF8.GetString(p, len);
}
catch (ArgumentException)
{
throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
}
}
}
}
}
#pragma warning restore 0649
private static void Verify(bool condition, MachOFormatError error)
{
if (!condition)
{
throw new AppHostMachOFormatException(error);
}
}
public static bool IsMachOImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < 256) // Header size
{
return false;
}
uint magic = reader.ReadUInt32();
return Enum.IsDefined(typeof(Magic), magic);
}
}
/// <summary>
/// This Method is a utility to remove the code-signature (if any)
/// from a MachO AppHost binary.
///
/// The tool assumes the following layout of the executable:
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
/// ...
/// LC_CODE_SIGNATURE (last)
///
/// * ... Different Segments ...
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB
/// * (Some alignment bytes)
/// * The Code-signature
///
/// In order to remove the signature, the method:
/// - Removes (zeros out) the LC_CODE_SIGNATURE command
/// - Adjusts the size and count of the load commands in the header
/// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
/// - Truncates the apphost file to the end of the __LINKEDIT segment
///
/// </summary>
/// <param name="stream">Stream containing the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - It is a signed binary, and
/// - The signature was successfully removed
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
public static unsafe bool RemoveSignature(FileStream stream)
{
uint signatureSize = 0;
using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
mapName: null,
capacity: 0,
MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None,
leaveOpen: true))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
MachHeader* header = (MachHeader*)file;
if (!header->IsValid())
{
// Not a MachO file.
return false;
}
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;
for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
else if (command->cmd == Command.LC_CODE_SIGNATURE)
{
Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
signature = (LinkEditDataCommand*)command;
break;
}
file += command->cmdsize;
}
if (signature != null)
{
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);
var symtabEnd = symtab->stroff + symtab->strsize;
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
var signatureEnd = signature->dataoff + signature->datasize;
var fileEnd = (ulong)stream.Length;
Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);
Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);
// The signature blob immediately follows the symtab blob,
// except for a few bytes of padding.
Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);
// Remove the signature command
header->ncmds--;
header->sizeofcmds -= signature->cmdsize;
Unsafe.InitBlock(signature, 0, signature->cmdsize);
// Remove the signature blob (note for truncation)
signatureSize = (uint)(fileEnd - symtabEnd);
// Adjust the __LINKEDIT segment load command
linkEdit->filesize -= signatureSize;
// codesign --remove-signature doesn't reset the vmsize.
// Setting the vmsize here makes the output bin-equal with the original
// unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
linkEdit->vmsize = linkEdit->filesize;
}
}
finally
{
if (file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}
if (signatureSize != 0)
{
// The signature was removed, update the file length
stream.SetLength(stream.Length - signatureSize);
return true;
}
return false;
}
/// <summary>
/// This Method is a utility to adjust the apphost MachO-header
/// to include the bytes added by the single-file bundler at the end of the file.
///
/// The tool assumes the following layout of the executable
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
///
/// * ... Different Segments
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB (last)
///
/// The MAC codesign tool places several restrictions on the layout
/// * The __LINKEDIT segment must be the last one
/// * The __LINKEDIT segment must cover the end of the file
/// * All bytes in the __LINKEDIT segment are used by other linkage commands
/// (ex: symbol/string table, dynamic load information etc)
///
/// In order to circumvent these restrictions, we:
/// * Extend the __LINKEDIT segment to include the bundle-data
/// * Extend the string table to include all the bundle-data
/// (that is, the bundle-data appear as strings to the loader/codesign tool).
///
/// This method has certain limitations:
/// * The bytes for the bundler may be unnecessarily loaded at startup
/// * Tools that process the string table may be confused (?)
/// * The string table size is limited to 4GB. Bundles larger than that size
/// cannot be accomodated by this utility.
///
/// </summary>
/// <param name="filePath">Path to the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - The additional bytes were successfully accomodated within the MachO segments.
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
public static unsafe bool AdjustHeadersForBundle(string filePath)
{
ulong fileLength = (ulong)new FileInfo(filePath).Length;
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
MachHeader* header = (MachHeader*)file;
if (!header->IsValid())
{
// Not a MachO file.
return false;
}
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;
for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
file += command->cmdsize;
}
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);
// Update the string table to include bundle-data
ulong newStringTableSize = fileLength - symtab->stroff;
if (newStringTableSize > uint.MaxValue)
{
// Too big, too bad;
return false;
}
symtab->strsize = (uint)newStringTableSize;
// Update the __LINKEDIT segment to include bundle-data
linkEdit->filesize = fileLength - linkEdit->fileoff;
linkEdit->vmsize = linkEdit->filesize;
}
finally
{
if (file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}
return true;
}
}
}

View File

@@ -0,0 +1,180 @@
// 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;
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 sybsystem 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>
/// <returns>true if the accessor represents a PE image, false otherwise.</returns>
internal static unsafe bool IsPEImage(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// 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))
{
return false;
}
return true;
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static bool IsPEImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint))
{
return false;
}
ushort signature = reader.ReadUInt16();
return signature == PEFileSignature;
}
}
/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
{
throw new AppHostNotPEFileException();
}
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
// 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)
{
throw new AppHostNotCUIException();
}
// Set the subsystem to GUI
subsystem[0] = WindowsGUISubsystem;
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static unsafe void SetWindowsGraphicalUserInterfaceBit(string filePath)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
SetWindowsGraphicalUserInterfaceBit(accessor);
}
}
}
/// <summary>
/// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe ushort GetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
{
throw new AppHostNotPEFileException();
}
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
return subsystem[0];
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static unsafe ushort GetWindowsGraphicalUserInterfaceBit(string filePath)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
return GetWindowsGraphicalUserInterfaceBit(accessor);
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// Unable to use input file as a valid application host executable, as it does not contain
/// the expected placeholder byte sequence.
/// </summary>
public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException
{
public byte[] MissingPattern { get; }
public PlaceHolderNotFoundInAppHostException(byte[] pattern)
{
MissingPattern = pattern;
}
}
}

View File

@@ -0,0 +1,98 @@
// 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;
using System.Text;
using System.Threading;
namespace Microsoft.NET.HostModel
{
/// <summary>
/// HostModel library implements several services for updating the AppHost DLL.
/// These updates involve multiple file open/close operations.
/// An Antivirus scanner may intercept in-between and lock the file,
/// causing the operations to fail with IO-Error.
/// So, the operations are retried a few times on failures such as
/// - IOException
/// - Failure with Win32 errors indicating file-lock
/// </summary>
public static class RetryUtil
{
public const int NumberOfRetries = 500;
public const int NumMilliSecondsToWait = 100;
public static void RetryOnIOError(Action func)
{
for (int i = 1; i <= NumberOfRetries; i++)
{
try
{
func();
break;
}
catch (IOException) when (i < NumberOfRetries)
{
Thread.Sleep(NumMilliSecondsToWait);
}
}
}
public static void RetryOnWin32Error(Action func)
{
static bool IsKnownIrrecoverableError(int hresult)
{
// Error codes are defined in winerror.h
// The error code is stored in the lowest 16 bits of the HResult
switch (hresult & 0xffff)
{
case 0x00000001: // ERROR_INVALID_FUNCTION
case 0x00000002: // ERROR_FILE_NOT_FOUND
case 0x00000003: // ERROR_PATH_NOT_FOUND
case 0x00000006: // ERROR_INVALID_HANDLE
case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY
case 0x0000000B: // ERROR_BAD_FORMAT
case 0x0000000E: // ERROR_OUTOFMEMORY
case 0x0000000F: // ERROR_INVALID_DRIVE
case 0x00000012: // ERROR_NO_MORE_FILES
case 0x00000035: // ERROR_BAD_NETPATH
case 0x00000057: // ERROR_INVALID_PARAMETER
case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES
case 0x00000072: // ERROR_INVALID_TARGET_HANDLE
case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED
case 0x0000007B: // ERROR_INVALID_NAME
case 0x0000007C: // ERROR_INVALID_LEVEL
case 0x0000007D: // ERROR_NO_VOLUME_LABEL
case 0x0000009A: // ERROR_LABEL_TOO_LONG
case 0x000000A0: // ERROR_BAD_ARGUMENTS
case 0x000000A1: // ERROR_BAD_PATHNAME
case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE
case 0x000000DF: // ERROR_FILE_TOO_LARGE
case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME
case 0x000003EE: // ERROR_FILE_INVALID
case 0x00000651: // ERROR_DEVICE_REMOVED
return true;
default:
return false;
}
}
for (int i = 1; i <= NumberOfRetries; i++)
{
try
{
func();
break;
}
catch (HResultException hrex)
when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult))
{
Thread.Sleep(NumMilliSecondsToWait);
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// BundleOptions: Optional settings for configuring the type of files
/// included in the single file bundle.
/// </summary>
[Flags]
public enum BundleOptions
{
None = 0,
BundleNativeBinaries = 1,
BundleOtherFiles = 2,
BundleSymbolFiles = 4,
BundleAllContent = BundleNativeBinaries | BundleOtherFiles,
EnableCompression = 8,
};
}

View File

@@ -0,0 +1,342 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using Microsoft.NET.HostModel.AppHost;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Bundler: Functionality to embed the managed app and its dependencies
/// into the host native binary.
/// </summary>
public class Bundler
{
public const uint BundlerMajorVersion = 6;
public const uint BundlerMinorVersion = 0;
private readonly string HostName;
private readonly string OutputDir;
private readonly string DepsJson;
private readonly string RuntimeConfigJson;
private readonly string RuntimeConfigDevJson;
private readonly Trace Tracer;
public readonly Manifest BundleManifest;
private readonly TargetInfo Target;
private readonly BundleOptions Options;
public Bundler(string hostName,
string outputDir,
BundleOptions options = BundleOptions.None,
OSPlatform? targetOS = null,
Architecture? targetArch = null,
Version targetFrameworkVersion = null,
bool diagnosticOutput = false,
string appAssemblyName = null)
{
Tracer = new Trace(diagnosticOutput);
HostName = hostName;
OutputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir);
Target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion);
if (Target.BundleMajorVersion < 6 &&
(options & BundleOptions.EnableCompression) != 0)
{
throw new ArgumentException("Compression requires framework version 6.0 or above", nameof(options));
}
appAssemblyName ??= Target.GetAssemblyName(hostName);
DepsJson = appAssemblyName + ".deps.json";
RuntimeConfigJson = appAssemblyName + ".runtimeconfig.json";
RuntimeConfigDevJson = appAssemblyName + ".runtimeconfig.dev.json";
BundleManifest = new Manifest(Target.BundleMajorVersion, netcoreapp3CompatMode: options.HasFlag(BundleOptions.BundleAllContent));
Options = Target.DefaultOptions | options;
}
private bool ShouldCompress(FileType type)
{
if (!Options.HasFlag(BundleOptions.EnableCompression))
{
return false;
}
switch (type)
{
case FileType.DepsJson:
case FileType.RuntimeConfigJson:
return false;
default:
return true;
}
}
/// <summary>
/// Embed 'file' into 'bundle'
/// </summary>
/// <returns>
/// startOffset: offset of the start 'file' within 'bundle'
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
/// </returns>
private (long startOffset, long compressedSize) AddToBundle(Stream bundle, Stream file, FileType type)
{
long startOffset = bundle.Position;
if (ShouldCompress(type))
{
long fileLength = file.Length;
file.Position = 0;
// We use DeflateStream here.
// It uses GZip algorithm, but with a trivial header that does not contain file info.
using (DeflateStream compressionStream = new DeflateStream(bundle, CompressionLevel.Optimal, leaveOpen: true))
{
file.CopyTo(compressionStream);
}
long compressedSize = bundle.Position - startOffset;
if (compressedSize < fileLength * 0.75)
{
return (startOffset, compressedSize);
}
// compression rate was not good enough
// roll back the bundle offset and let the uncompressed code path take care of the entry.
bundle.Seek(startOffset, SeekOrigin.Begin);
}
if (type == FileType.Assembly)
{
long misalignment = (bundle.Position % Target.AssemblyAlignment);
if (misalignment != 0)
{
long padding = Target.AssemblyAlignment - misalignment;
bundle.Position += padding;
}
}
file.Position = 0;
startOffset = bundle.Position;
file.CopyTo(bundle);
return (startOffset, 0);
}
private bool IsHost(string fileRelativePath)
{
return fileRelativePath.Equals(HostName);
}
private bool ShouldIgnore(string fileRelativePath)
{
return fileRelativePath.Equals(RuntimeConfigDevJson);
}
private bool ShouldExclude(FileType type, string relativePath)
{
switch (type)
{
case FileType.Assembly:
case FileType.DepsJson:
case FileType.RuntimeConfigJson:
return false;
case FileType.NativeBinary:
return !Options.HasFlag(BundleOptions.BundleNativeBinaries) || Target.ShouldExclude(relativePath);
case FileType.Symbols:
return !Options.HasFlag(BundleOptions.BundleSymbolFiles);
case FileType.Unknown:
return !Options.HasFlag(BundleOptions.BundleOtherFiles);
default:
Debug.Assert(false);
return false;
}
}
private bool IsAssembly(string path, out bool isPE)
{
isPE = false;
using (FileStream file = File.OpenRead(path))
{
try
{
PEReader peReader = new PEReader(file);
CorHeader corHeader = peReader.PEHeaders.CorHeader;
isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage
return corHeader != null;
}
catch (BadImageFormatException)
{
}
}
return false;
}
private FileType InferType(FileSpec fileSpec)
{
if (fileSpec.BundleRelativePath.Equals(DepsJson))
{
return FileType.DepsJson;
}
if (fileSpec.BundleRelativePath.Equals(RuntimeConfigJson))
{
return FileType.RuntimeConfigJson;
}
if (Path.GetExtension(fileSpec.BundleRelativePath).ToLowerInvariant().Equals(".pdb"))
{
return FileType.Symbols;
}
bool isPE;
if (IsAssembly(fileSpec.SourcePath, out isPE))
{
return FileType.Assembly;
}
bool isNativeBinary = Target.IsWindows ? isPE : Target.IsNativeBinary(fileSpec.SourcePath);
if (isNativeBinary)
{
return FileType.NativeBinary;
}
return FileType.Unknown;
}
/// <summary>
/// Generate a bundle, given the specification of embedded files
/// </summary>
/// <param name="fileSpecs">
/// An enumeration FileSpecs for the files to be embedded.
///
/// Files in fileSpecs that are not bundled within the single file bundle,
/// and should be published as separate files are marked as "IsExcluded" by this method.
/// This doesn't include unbundled files that should be dropped, and not publised as output.
/// </param>
/// <returns>
/// The full path the the generated bundle file
/// </returns>
/// <exceptions>
/// ArgumentException if input is invalid
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
/// </exceptions>
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
{
Tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}");
Tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}");
Tracer.Log($"Target Runtime: {Target}");
Tracer.Log($"Bundler Options: {Options}");
if (fileSpecs.Any(x => !x.IsValid()))
{
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
}
string hostSource;
try
{
hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(HostName)).Single().SourcePath;
}
catch (InvalidOperationException)
{
throw new ArgumentException("Invalid input specification: Must specify the host binary");
}
string bundlePath = Path.Combine(OutputDir, HostName);
if (File.Exists(bundlePath))
{
Tracer.Log($"Ovewriting existing File {bundlePath}");
}
BinaryUtils.CopyFile(hostSource, bundlePath);
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
long headerOffset = 0;
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
{
Stream bundle = writer.BaseStream;
bundle.Position = bundle.Length;
foreach (var fileSpec in fileSpecs)
{
string relativePath = fileSpec.BundleRelativePath;
if (IsHost(relativePath))
{
continue;
}
if (ShouldIgnore(relativePath))
{
Tracer.Log($"Ignore: {relativePath}");
continue;
}
FileType type = InferType(fileSpec);
if (ShouldExclude(type, relativePath))
{
Tracer.Log($"Exclude [{type}]: {relativePath}");
fileSpec.Excluded = true;
continue;
}
if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
{
if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
{
throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
}
// Exact duplicate - intentionally skip and don't include a second copy in the bundle
continue;
}
else
{
relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
}
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
{
FileType targetType = Target.TargetSpecificFileType(type);
(long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion);
Tracer.Log($"Embed: {entry}");
}
}
// Write the bundle manifest
headerOffset = BundleManifest.Write(writer);
Tracer.Log($"Header Offset={headerOffset}");
Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}");
}
HostWriter.SetAsBundle(bundlePath, headerOffset);
return bundlePath;
}
}
}

View File

@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// FileEntry: Records information about embedded files.
///
/// The bundle manifest records the following meta-data for each
/// file embedded in the bundle:
/// * Type (1 byte)
/// * NameLength (7-bit extension encoding, typically 1 byte)
/// * Name ("NameLength" Bytes)
/// * Offset (Int64)
/// * Size (Int64)
/// === present only in bundle version 3+
/// * CompressedSize (Int64) 0 indicates No Compression
/// </summary>
public class FileEntry
{
public readonly uint BundleMajorVersion;
public readonly long Offset;
public readonly long Size;
public readonly long CompressedSize;
public readonly FileType Type;
public readonly string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
public const char DirectorySeparatorChar = '/';
public FileEntry(FileType fileType, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion)
{
BundleMajorVersion = bundleMajorVersion;
Type = fileType;
RelativePath = relativePath.Replace('\\', DirectorySeparatorChar);
Offset = offset;
Size = size;
CompressedSize = compressedSize;
}
public void Write(BinaryWriter writer)
{
writer.Write(Offset);
writer.Write(Size);
// compression is used only in version 6.0+
if (BundleMajorVersion >= 6)
{
writer.Write(CompressedSize);
}
writer.Write((byte)Type);
writer.Write(RelativePath);
}
public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}";
}
}

View File

@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Information about files to embed into the Bundle (input to the Bundler).
///
/// SourcePath: path to the file to be bundled at compile time
/// BundleRelativePath: path where the file is expected at run time,
/// relative to the app DLL.
/// </summary>
public class FileSpec
{
public readonly string SourcePath;
public readonly string BundleRelativePath;
public bool Excluded;
public FileSpec(string sourcePath, string bundleRelativePath)
{
SourcePath = sourcePath;
BundleRelativePath = bundleRelativePath;
Excluded = false;
}
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(SourcePath) &&
!string.IsNullOrWhiteSpace(BundleRelativePath);
}
public override string ToString() => $"SourcePath: {SourcePath}, RelativePath: {BundleRelativePath} {(Excluded ? "[Excluded]" : "")}";
}
}

View File

@@ -0,0 +1,21 @@
// 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.Bundle
{
/// <summary>
/// FileType: Identifies the type of file embedded into the bundle.
///
/// The bundler differentiates a few kinds of files via the manifest,
/// with respect to the way in which they'll be used by the runtime.
/// </summary>
public enum FileType : byte
{
Unknown, // Type not determined.
Assembly, // IL and R2R Assemblies
NativeBinary, // NativeBinaries
DepsJson, // .deps.json configuration file
RuntimeConfigJson, // .runtimeconfig.json configuration file
Symbols // PDB Files
};
}

View File

@@ -0,0 +1,177 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// BundleManifest is a description of the contents of a bundle file.
/// This class handles creation and consumption of bundle-manifests.
///
/// Here is the description of the Bundle Layout:
/// _______________________________________________
/// AppHost
///
///
/// ------------Embedded Files ---------------------
/// The embedded files including the app, its
/// configuration files, dependencies, and
/// possibly the runtime.
///
///
///
///
///
///
///
/// ------------ Bundle Header -------------
/// MajorVersion
/// MinorVersion
/// NumEmbeddedFiles
/// ExtractionID
/// DepsJson Location [Version 2+]
/// Offset
/// Size
/// RuntimeConfigJson Location [Version 2+]
/// Offset
/// Size
/// Flags [Version 2+]
/// - - - - - - Manifest Entries - - - - - - - - - - -
/// Series of FileEntries (for each embedded file)
/// [File Type, Name, Offset, Size information]
///
///
///
/// _________________________________________________
/// </summary>
public class Manifest
{
// NetcoreApp3CompatMode flag is set on a .net5 app,
// which chooses to build single-file apps in .netcore3.x compat mode,
// by constructing the bundler with BundleAllConent option.
// This mode is expected to be deprecated in future versions of .NET.
[Flags]
private enum HeaderFlags : ulong
{
None = 0,
NetcoreApp3CompatMode = 1
}
// Bundle ID is a string that is used to uniquely
// identify this bundle. It is choosen to be compatible
// with path-names so that the AppHost can use it in
// extraction path.
public string BundleID { get; private set; }
//Same as Path.GetRandomFileName
private const int BundleIdLength = 12;
private SHA256 bundleHash = SHA256.Create();
public readonly uint BundleMajorVersion;
// The Minor version is currently unused, and is always zero
public const uint BundleMinorVersion = 0;
private FileEntry DepsJsonEntry;
private FileEntry RuntimeConfigJsonEntry;
private HeaderFlags Flags;
public List<FileEntry> Files;
public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";
public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
{
BundleMajorVersion = bundleMajorVersion;
Files = new List<FileEntry>();
Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
}
public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion)
{
if (bundleHash == null)
{
throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained.");
}
FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion);
Files.Add(entry);
fileContent.Position = 0;
byte[] hashBytes = ComputeSha256Hash(fileContent);
bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0);
switch (entry.Type)
{
case FileType.DepsJson:
DepsJsonEntry = entry;
break;
case FileType.RuntimeConfigJson:
RuntimeConfigJsonEntry = entry;
break;
case FileType.Assembly:
break;
default:
break;
}
return entry;
}
private static byte[] ComputeSha256Hash(Stream stream)
{
using (SHA256 sha = SHA256.Create())
{
return sha.ComputeHash(stream);
}
}
private string GenerateDeterministicId()
{
bundleHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
byte[] manifestHash = bundleHash.Hash;
bundleHash.Dispose();
bundleHash = null;
return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_');
}
public long Write(BinaryWriter writer)
{
BundleID = BundleID ?? GenerateDeterministicId();
long startOffset = writer.BaseStream.Position;
// Write the bundle header
writer.Write(BundleMajorVersion);
writer.Write(BundleMinorVersion);
writer.Write(Files.Count);
writer.Write(BundleID);
if (BundleMajorVersion >= 2)
{
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0);
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0);
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0);
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0);
writer.Write((ulong)Flags);
}
// Write the manifest entries
foreach (FileEntry entry in Files)
{
entry.Write(writer);
}
return startOffset;
}
public bool Contains(string relativePath)
{
return Files.Any(entry => relativePath.Equals(entry.RelativePath));
}
}
}

View File

@@ -0,0 +1,120 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.NET.HostModel.AppHost;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// TargetInfo: Information about the target for which the single-file bundle is built.
///
/// Currently the TargetInfo only tracks:
/// - the target operating system
/// - the target architecture
/// - the target framework
/// - the default options for this target
/// - the assembly alignment for this target
/// </summary>
public class TargetInfo
{
public readonly OSPlatform OS;
public readonly Architecture Arch;
public readonly Version FrameworkVersion;
public readonly uint BundleMajorVersion;
public readonly BundleOptions DefaultOptions;
public readonly int AssemblyAlignment;
public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVersion)
{
OS = os ?? HostOS;
Arch = arch ?? RuntimeInformation.OSArchitecture;
FrameworkVersion = targetFrameworkVersion ?? net60;
Debug.Assert(IsLinux || IsOSX || IsWindows);
if (FrameworkVersion.CompareTo(net60) >= 0)
{
BundleMajorVersion = 6u;
DefaultOptions = BundleOptions.None;
}
else if (FrameworkVersion.CompareTo(net50) >= 0)
{
BundleMajorVersion = 2u;
DefaultOptions = BundleOptions.None;
}
else if (FrameworkVersion.Major == 3 && (FrameworkVersion.Minor == 0 || FrameworkVersion.Minor == 1))
{
BundleMajorVersion = 1u;
DefaultOptions = BundleOptions.BundleAllContent;
}
else
{
throw new ArgumentException($"Invalid input: Unsupported Target Framework Version {targetFrameworkVersion}");
}
if (IsLinux && Arch == Architecture.Arm64)
{
// We align assemblies in the bundle at 4K so that we can use mmap on Linux without changing the page alignment of ARM64 R2R code.
// This is only necessary for R2R assemblies, but we do it for all assemblies for simplicity.
// See https://github.com/dotnet/runtime/issues/41832.
AssemblyAlignment = 4096;
}
else
{
// Otherwise, assemblies are 16 bytes aligned, so that their sections can be memory-mapped cache aligned.
AssemblyAlignment = 16;
}
}
public bool IsNativeBinary(string filePath)
{
return IsLinux ? ElfUtils.IsElfImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : PEUtils.IsPEImage(filePath);
}
public string GetAssemblyName(string hostName)
{
// This logic to calculate assembly name from hostName should be removed (and probably moved to test helpers)
// once the SDK in the correct assembly name.
return (IsWindows ? Path.GetFileNameWithoutExtension(hostName) : hostName);
}
public override string ToString()
{
string os = IsWindows ? "win" : IsLinux ? "linux" : "osx";
string arch = Arch.ToString().ToLowerInvariant();
return $"OS: {os} Arch: {arch} FrameworkVersion: {FrameworkVersion}";
}
private static OSPlatform HostOS => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OSPlatform.Linux :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OSPlatform.OSX : OSPlatform.Windows;
public bool IsLinux => OS.Equals(OSPlatform.Linux);
public bool IsOSX => OS.Equals(OSPlatform.OSX);
public bool IsWindows => OS.Equals(OSPlatform.Windows);
// The .net core 3 apphost doesn't care about semantics of FileType -- all files are extracted at startup.
// However, the apphost checks that the FileType value is within expected bounds, so set it to the first enumeration.
public FileType TargetSpecificFileType(FileType fileType) => (BundleMajorVersion == 1) ? FileType.Unknown : fileType;
// In .net core 3.x, bundle processing happens within the AppHost.
// Therefore HostFxr and HostPolicy can be bundled within the single-file app.
// In .net 5, bundle processing happens in HostFxr and HostPolicy libraries.
// Therefore, these libraries themselves cannot be bundled into the single-file app.
// This problem is mitigated by statically linking these host components with the AppHost.
// https://github.com/dotnet/runtime/issues/32823
public bool ShouldExclude(string relativePath) =>
(FrameworkVersion.Major != 3) && (relativePath.Equals(HostFxr) || relativePath.Equals(HostPolicy));
private readonly Version net60 = new Version(6, 0);
private readonly Version net50 = new Version(5, 0);
private string HostFxr => IsWindows ? "hostfxr.dll" : IsLinux ? "libhostfxr.so" : "libhostfxr.dylib";
private string HostPolicy => IsWindows ? "hostpolicy.dll" : IsLinux ? "libhostpolicy.so" : "libhostpolicy.dylib";
}
}

View File

@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Tracing utilities for diagnostic output
/// </summary>
public class Trace
{
private readonly bool Verbose;
public Trace(bool verbose)
{
Verbose = verbose;
}
public void Log(string fmt, params object[] args)
{
if (Verbose)
{
Console.WriteLine("LOG: " + fmt, args);
}
}
public void Error(string type, string message)
{
Console.Error.WriteLine($"ERROR: {message}");
}
}
}

View File

@@ -0,0 +1,304 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.NET.HostModel.ComHost
{
public static class ClsidMap
{
private struct ClsidEntry
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("assembly")]
public string Assembly { get; set; }
[JsonPropertyName("progid")]
public string ProgId { get; set; }
}
public static void Create(MetadataReader metadataReader, string clsidMapPath)
{
Dictionary<string, ClsidEntry> clsidMap = new Dictionary<string, ClsidEntry>();
string assemblyName = GetAssemblyName(metadataReader).FullName;
bool isAssemblyComVisible = IsComVisible(metadataReader, metadataReader.GetAssemblyDefinition());
foreach (TypeDefinitionHandle type in metadataReader.TypeDefinitions)
{
TypeDefinition definition = metadataReader.GetTypeDefinition(type);
// Only public COM-visible classes can be exposed via the COM host.
if (TypeIsPublic(metadataReader, definition) && TypeIsClass(metadataReader, definition) && IsComVisible(metadataReader, definition, isAssemblyComVisible))
{
Guid guid = GetTypeGuid(metadataReader, definition);
string guidString = GetTypeGuid(metadataReader, definition).ToString("B");
if (clsidMap.ContainsKey(guidString))
{
throw new ConflictingGuidException(clsidMap[guidString].Type, GetTypeName(metadataReader, definition), guid);
}
string progId = GetProgId(metadataReader, definition);
clsidMap.Add(guidString,
new ClsidEntry
{
Type = GetTypeName(metadataReader, definition),
Assembly = assemblyName,
ProgId = !string.IsNullOrWhiteSpace(progId) ? progId : null
});
}
}
using (StreamWriter writer = File.CreateText(clsidMapPath))
{
writer.Write(JsonSerializer.Serialize(clsidMap, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
}
}
private static bool TypeIsClass(MetadataReader metadataReader, TypeDefinition definition)
{
if ((definition.Attributes & TypeAttributes.Interface) != 0)
{
return false;
}
EntityHandle baseTypeEntity = definition.BaseType;
if (baseTypeEntity.Kind == HandleKind.TypeReference)
{
TypeReference baseClass = metadataReader.GetTypeReference((TypeReferenceHandle)baseTypeEntity);
if (baseClass.ResolutionScope.Kind == HandleKind.AssemblyReference)
{
if (HasTypeName(metadataReader, baseClass, "System", "ValueType") || HasTypeName(metadataReader, baseClass, "System", "Enum"))
{
return false;
}
}
}
return true;
}
private static bool TypeIsPublic(MetadataReader reader, TypeDefinition type)
{
switch (type.Attributes & TypeAttributes.VisibilityMask)
{
case TypeAttributes.Public:
return true;
case TypeAttributes.NestedPublic:
return TypeIsPublic(reader, reader.GetTypeDefinition(type.GetDeclaringType()));
default:
return false;
}
}
private static string GetTypeName(MetadataReader metadataReader, TypeDefinition type)
{
if (!type.GetDeclaringType().IsNil)
{
return $"{GetTypeName(metadataReader, metadataReader.GetTypeDefinition(type.GetDeclaringType()))}+{metadataReader.GetString(type.Name)}";
}
return $"{metadataReader.GetString(type.Namespace)}{Type.Delimiter}{metadataReader.GetString(type.Name)}";
}
private static bool HasTypeName(MetadataReader metadataReader, TypeReference type, string ns, string name)
{
return metadataReader.StringComparer.Equals(type.Namespace, ns) && metadataReader.StringComparer.Equals(type.Name, name);
}
private static AssemblyName GetAssemblyName(MetadataReader metadataReader)
{
AssemblyName name = new AssemblyName();
AssemblyDefinition definition = metadataReader.GetAssemblyDefinition();
name.Name = metadataReader.GetString(definition.Name);
name.Version = definition.Version;
name.CultureInfo = CultureInfo.GetCultureInfo(metadataReader.GetString(definition.Culture));
name.SetPublicKey(metadataReader.GetBlobBytes(definition.PublicKey));
return name;
}
private static bool IsComVisible(MetadataReader reader, AssemblyDefinition assembly)
{
CustomAttributeHandle handle = GetComVisibleAttribute(reader, assembly.GetCustomAttributes());
if (handle.IsNil)
{
return false;
}
CustomAttribute comVisibleAttribute = reader.GetCustomAttribute(handle);
CustomAttributeValue<KnownType> data = comVisibleAttribute.DecodeValue(new TypeResolver());
return (bool)data.FixedArguments[0].Value;
}
private static bool IsComVisible(MetadataReader metadataReader, TypeDefinition definition, bool assemblyComVisible)
{
// We need to ensure that all parent scopes of the given type are not explicitly non-ComVisible.
bool? IsComVisibleCore(TypeDefinition typeDefinition)
{
CustomAttributeHandle handle = GetComVisibleAttribute(metadataReader, typeDefinition.GetCustomAttributes());
if (handle.IsNil)
{
return null;
}
CustomAttribute comVisibleAttribute = metadataReader.GetCustomAttribute(handle);
CustomAttributeValue<KnownType> data = comVisibleAttribute.DecodeValue(new TypeResolver());
return (bool)data.FixedArguments[0].Value;
}
if (!definition.GetDeclaringType().IsNil)
{
return IsComVisible(metadataReader, metadataReader.GetTypeDefinition(definition.GetDeclaringType()), assemblyComVisible) && (IsComVisibleCore(definition) ?? assemblyComVisible);
}
return IsComVisibleCore(definition) ?? assemblyComVisible;
}
private static CustomAttributeHandle GetComVisibleAttribute(MetadataReader reader, CustomAttributeHandleCollection customAttributes)
{
foreach (CustomAttributeHandle attr in customAttributes)
{
CustomAttribute attribute = reader.GetCustomAttribute(attr);
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "ComVisibleAttribute"))
{
return attr;
}
}
return default;
}
private static Guid GetTypeGuid(MetadataReader reader, TypeDefinition type)
{
// Find the class' GUID by reading the GuidAttribute value.
// We do not support implicit runtime-generated GUIDs for the .NET Core COM host.
foreach (CustomAttributeHandle attr in type.GetCustomAttributes())
{
CustomAttribute attribute = reader.GetCustomAttribute(attr);
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "GuidAttribute"))
{
CustomAttributeValue<KnownType> data = attribute.DecodeValue(new TypeResolver());
return Guid.Parse((string)data.FixedArguments[0].Value);
}
}
throw new MissingGuidException(GetTypeName(reader, type));
}
private static string GetProgId(MetadataReader reader, TypeDefinition type)
{
foreach (CustomAttributeHandle attr in type.GetCustomAttributes())
{
CustomAttribute attribute = reader.GetCustomAttribute(attr);
if (IsTargetAttribute(reader, attribute, "System.Runtime.InteropServices", "ProgIdAttribute"))
{
CustomAttributeValue<KnownType> data = attribute.DecodeValue(new TypeResolver());
return (string)data.FixedArguments[0].Value;
}
}
return GetTypeName(reader, type);
}
private static bool IsTargetAttribute(MetadataReader reader, CustomAttribute attribute, string targetNamespace, string targetName)
{
StringHandle namespaceMaybe;
StringHandle nameMaybe;
switch (attribute.Constructor.Kind)
{
case HandleKind.MemberReference:
MemberReference refConstructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor);
TypeReference refType = reader.GetTypeReference((TypeReferenceHandle)refConstructor.Parent);
namespaceMaybe = refType.Namespace;
nameMaybe = refType.Name;
break;
case HandleKind.MethodDefinition:
MethodDefinition defConstructor = reader.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor);
TypeDefinition defType = reader.GetTypeDefinition(defConstructor.GetDeclaringType());
namespaceMaybe = defType.Namespace;
nameMaybe = defType.Name;
break;
default:
Debug.Assert(false, "Unknown attribute constructor kind");
return false;
}
return reader.StringComparer.Equals(namespaceMaybe, targetNamespace) && reader.StringComparer.Equals(nameMaybe, targetName);
}
private enum KnownType
{
Bool,
String,
SystemType,
Unknown
}
private class TypeResolver : ICustomAttributeTypeProvider<KnownType>
{
public KnownType GetPrimitiveType(PrimitiveTypeCode typeCode)
{
switch (typeCode)
{
case PrimitiveTypeCode.Boolean:
return KnownType.Bool;
case PrimitiveTypeCode.String:
return KnownType.String;
default:
return KnownType.Unknown;
}
}
public KnownType GetSystemType()
{
return KnownType.SystemType;
}
public KnownType GetSZArrayType(KnownType elementType)
{
return KnownType.Unknown;
}
public KnownType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
{
return KnownType.Unknown;
}
public KnownType GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
{
return KnownType.Unknown;
}
public KnownType GetTypeFromSerializedName(string name)
{
return KnownType.Unknown;
}
public PrimitiveTypeCode GetUnderlyingEnumType(KnownType type)
{
throw new BadImageFormatException("Unexpectedly got an enum parameter for an attribute.");
}
public bool IsSystemType(KnownType type)
{
return type == KnownType.SystemType;
}
}
}
}

View File

@@ -0,0 +1,80 @@
// 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.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.NET.HostModel.ComHost
{
public class ComHost
{
private const int E_INVALIDARG = unchecked((int)0x80070057);
// These need to match RESOURCEID_CLSIDMAP and RESOURCETYPE_CLSIDMAP defined in comhost.h.
private const int ClsidmapResourceId = 64;
private const int ClsidmapResourceType = 1024;
/// <summary>
/// Create a ComHost with an embedded CLSIDMap file to map CLSIDs to .NET Classes.
/// </summary>
/// <param name="comHostSourceFilePath">The path of Apphost template, which has the place holder</param>
/// <param name="comHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
/// <param name="clsidmapFilePath">The path to the *.clsidmap file.</param>
/// <param name="typeLibraries">Resource ids for tlbs and paths to the tlb files to be embedded.</param>
public static void Create(
string comHostSourceFilePath,
string comHostDestinationFilePath,
string clsidmapFilePath,
IReadOnlyDictionary<int, string> typeLibraries = null)
{
var destinationDirectory = new FileInfo(comHostDestinationFilePath).Directory.FullName;
if (!Directory.Exists(destinationDirectory))
{
Directory.CreateDirectory(destinationDirectory);
}
// Copy apphost to destination path so it inherits the same attributes/permissions.
File.Copy(comHostSourceFilePath, comHostDestinationFilePath, overwrite: true);
if (!ResourceUpdater.IsSupportedOS())
{
throw new ComHostCustomizationUnsupportedOSException();
}
string clsidMap = File.ReadAllText(clsidmapFilePath);
byte[] clsidMapBytes = Encoding.UTF8.GetBytes(clsidMap);
using (ResourceUpdater updater = new ResourceUpdater(comHostDestinationFilePath))
{
updater.AddResource(clsidMapBytes, (IntPtr)ClsidmapResourceType, (IntPtr)ClsidmapResourceId);
if (typeLibraries is not null)
{
foreach (var typeLibrary in typeLibraries)
{
if (!ResourceUpdater.IsIntResource((IntPtr)typeLibrary.Key))
{
throw new InvalidTypeLibraryIdException(typeLibrary.Value, typeLibrary.Key);
}
try
{
byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
updater.AddResource(tlbFileBytes, "typelib", (IntPtr)typeLibrary.Key);
}
catch (FileNotFoundException ex)
{
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
}
catch (HResultException hr) when (hr.Win32HResult == E_INVALIDARG)
{
throw new InvalidTypeLibraryException(typeLibrary.Value, hr);
}
}
}
updater.Update();
}
}
}
}

View File

@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The application host executable cannot be customized because adding resources requires
/// that the build be performed on Windows (excluding Nano Server).
/// </summary>
public class ComHostCustomizationUnsupportedOSException : Exception
{
}
}

View File

@@ -0,0 +1,34 @@
// 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.Collections.Generic;
using System.Text;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The same Guid has been specified for two public ComVisible classes in the assembly.
/// </summary>
public class ConflictingGuidException : Exception
{
public ConflictingGuidException(string typeName1, string typeName2, Guid guid)
{
if (typeName1 is null)
{
throw new ArgumentNullException(nameof(typeName1));
}
if (typeName2 is null)
{
throw new ArgumentNullException(nameof(typeName2));
}
TypeName1 = typeName1;
TypeName2 = typeName2;
Guid = guid;
}
public string TypeName1 { get; }
public string TypeName2 { get; }
public Guid Guid { get; }
}
}

View File

@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The provided type library file is an invalid format.
/// </summary>
public class InvalidTypeLibraryException : Exception
{
public InvalidTypeLibraryException(string path)
{
Path = path;
}
public InvalidTypeLibraryException(string path, Exception innerException)
:base($"Invalid type library at '{path}'.", innerException)
{
Path = path;
}
public string Path { get; }
}
}

View File

@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The provided resource id for the type library is unsupported.
/// </summary>
public class InvalidTypeLibraryIdException : Exception
{
public InvalidTypeLibraryIdException(string path, int id)
{
Path = path;
Id = id;
}
public string Path { get; }
public int Id { get; }
}
}

View File

@@ -0,0 +1,26 @@
// 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.Collections.Generic;
using System.Text;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The type <see cref="TypeName"/> is public and ComVisible but does not have a <see cref="System.Runtime.InteropServices.GuidAttribute"/> attribute.
/// </summary>
public class MissingGuidException : Exception
{
public MissingGuidException(string typeName)
{
if (typeName is null)
{
throw new ArgumentNullException(nameof(typeName));
}
TypeName = typeName;
}
public string TypeName { get; }
}
}

View File

@@ -0,0 +1,104 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Xml;
using System.Xml.Linq;
namespace Microsoft.NET.HostModel.ComHost
{
public class RegFreeComManifest
{
/// <summary>
/// Generates a side-by-side application manifest to enable reg-free COM.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <param name="comHostName">The name of the comhost library.</param>
/// <param name="assemblyVersion">The version of the assembly.</param>
/// <param name="clsidMapPath">The path to the clsidmap file.</param>
/// <param name="comManifestPath">The path to which to write the manifest.</param>
/// <param name="typeLibraries">The type libraries to include in the manifest.</param>
public static void CreateManifestFromClsidmap(string assemblyName, string comHostName, string assemblyVersion, string clsidMapPath, string comManifestPath, IReadOnlyDictionary<int, string> typeLibraries = null)
{
XNamespace ns = "urn:schemas-microsoft-com:asm.v1";
XElement manifest = new XElement(ns + "assembly", new XAttribute("manifestVersion", "1.0"));
manifest.Add(new XElement(ns + "assemblyIdentity",
new XAttribute("type", "win32"),
new XAttribute("name", $"{assemblyName}.X"),
new XAttribute("version", assemblyVersion)));
var fileElement = CreateComHostFileElement(clsidMapPath, comHostName, ns);
if (typeLibraries is not null)
{
AddTypeLibElementsToFileElement(typeLibraries, ns, fileElement);
}
manifest.Add(fileElement);
XDocument manifestDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), manifest);
XmlWriterSettings settings = new XmlWriterSettings()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
};
using (XmlWriter manifestWriter = XmlWriter.Create(comManifestPath, settings))
{
manifestDocument.WriteTo(manifestWriter);
}
}
private static XElement CreateComHostFileElement(string clsidMapPath, string comHostName, XNamespace ns)
{
XElement fileElement = new XElement(ns + "file", new XAttribute("name", comHostName));
JsonElement clsidMap;
using (FileStream clsidMapStream = File.OpenRead(clsidMapPath))
{
clsidMap = JsonDocument.Parse(clsidMapStream).RootElement;
}
foreach (JsonProperty property in clsidMap.EnumerateObject())
{
string guidMaybe = property.Name;
Guid guid = Guid.Parse(guidMaybe);
XElement comClassElement = new XElement(ns + "comClass", new XAttribute("clsid", guid.ToString("B")), new XAttribute("threadingModel", "Both"));
if (property.Value.TryGetProperty("progid", out JsonElement progIdValue))
{
comClassElement.Add(new XAttribute("progid", progIdValue.GetString()));
}
fileElement.Add(comClassElement);
}
return fileElement;
}
private static void AddTypeLibElementsToFileElement(IReadOnlyDictionary<int, string> typeLibraries, XNamespace ns, XElement fileElement)
{
foreach (var typeLibrary in typeLibraries)
{
try
{
byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
TypeLibReader reader = new TypeLibReader(tlbFileBytes);
if (!reader.TryReadTypeLibGuidAndVersion(out Guid name, out Version version))
{
throw new InvalidTypeLibraryException(typeLibrary.Value);
}
XElement typeLibElement = new XElement(ns + "typelib",
new XAttribute("tlbid", name.ToString("B")),
new XAttribute("resourceid", typeLibrary.Key),
new XAttribute("version", version),
new XAttribute("helpdir", ""));
fileElement.Add(typeLibElement);
}
catch (FileNotFoundException ex)
{
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
// 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.Buffers.Binary;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// Reads data from a COM Type Library file based on the official implementation in the Win32 function LoadTypeLib.
/// We do the reading ourselves instead of calling into the OS so we don't have to worry about the type library's
/// dependencies being discoverable on disk.
/// </summary>
internal class TypeLibReader
{
private byte[] tlbBytes;
public TypeLibReader(byte[] tlbBytes)
{
this.tlbBytes = tlbBytes;
}
private const int OffsetOfGuidOffset = sizeof(int) * 2;
private const int SizeOfGuidOffset = sizeof(int);
private const int OffsetOfMajorVersion = OffsetOfGuidOffset + SizeOfGuidOffset + sizeof(int) * 2 + sizeof(ushort) * 2;
private const int SizeOfMajorVersion = sizeof(ushort);
private const int OffsetOfMinorVersion = OffsetOfMajorVersion + SizeOfMajorVersion;
private const int SizeOfMinorVersion = sizeof(ushort);
private const int OffsetOfTypeInfosCount = OffsetOfMinorVersion + SizeOfMinorVersion + sizeof(int);
private const int SizeOfTypeInfosCount = sizeof(int);
private const int OffsetOfTablesStart = OffsetOfTypeInfosCount + SizeOfTypeInfosCount + sizeof(int) * 12;
private const int NumTablesToSkip = 5;
private const int SizeOfTableHeader = sizeof(int) * 4;
private Guid FindGuid(ReadOnlySpan<byte> fileContents)
{
checked
{
int typelibGuidEntryOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfGuidOffset));
int infoRefsOffsetCount = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfTypeInfosCount));
int infoBytes = infoRefsOffsetCount * SizeOfTypeInfosCount;
int guidTableOffset = OffsetOfTablesStart + infoBytes + SizeOfTableHeader * NumTablesToSkip;
int fileOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(guidTableOffset));
return new Guid(fileContents.Slice(fileOffset + typelibGuidEntryOffset, 16).ToArray());
}
}
public bool TryReadTypeLibGuidAndVersion(out Guid typelibId, out Version version)
{
typelibId = default;
version = default;
try
{
var span = new ReadOnlySpan<byte>(tlbBytes);
typelibId = FindGuid(span);
ushort majorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMajorVersion));
ushort minorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMinorVersion));
version = new Version(majorVer, minorVer);
return true;
}
catch (System.OverflowException)
{
return false;
}
catch (System.IndexOutOfRangeException)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The specified type library path does not exist.
/// </summary>
public class TypeLibraryDoesNotExistException : Exception
{
public TypeLibraryDoesNotExistException(string path, Exception innerException)
:base($"Type library '{path}' does not exist.", innerException)
{
Path = path;
}
public string Path { get; }
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>Abstractions for modifying .NET host binaries</Description>
<IsShipping>false</IsShipping>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<Serviceable>true</Serviceable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Managed API isn't completely documented yet. TODO: https://github.com/dotnet/core-setup/issues/5108 -->
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Historically, the key for the managed projects is the AspNetCore key Arcade carries. -->
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId Condition="'$(PgoInstrument)' == 'true'">Microsoft.Net.HostModel.PGO</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Reflection.Metadata" Version="1.8.0" />
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="$(SystemRuntimeCompilerServicesUnsafeVersion)" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
Host Model
===================================
HostModel is a library used by the [SDK](https://github.com/dotnet/sdk) to perform certain transformations on host executables. The main services implemented in HostModel are:
* AppHost rewriter: Embeds the App Name into the AppHost executable. On Windows, also copies resources from App.dll to the AppHost.
* ComHost rewriter: Creates a ComHost with an embedded CLSIDMap file to map CLSIDs to .NET Classes.
* Single-file bundler: Embeds an application and its dependencies into the AppHost, to publish a single executable, as described [here](https://github.com/dotnet/designs/blob/master/accepted/2020/single-file/design.md).
The HostModel library is in the Runtime repo because:
* The implementations of the host and HostModel are closely related, which facilitates easy development, update, and testing.
* Separating the HostModel implementation from SDK repo repo aligns with code ownership, and facilitates maintenance.
The build targets/tasks that use the HostModel library are in the SDK repo because:
* This facilitates the MSBuild tasks to be multi-targeted.
* It helps generate localized error messages, since SDK repo has the localization infrastructure.

View File

@@ -0,0 +1,491 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
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.
/// </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;
///<summary>
/// Determines if the ResourceUpdater is supported by the current operating system.
/// Some versions of Windows, such as Nano Server, do not support the needed APIs.
/// </summary>
public static bool IsSupportedOS()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
try
{
// On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error
// Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported
// 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.
/// </summary>
public ResourceUpdater(string peFile)
{
hUpdate = Kernel32.BeginUpdateResource(peFile, false);
if (hUpdate.IsInvalid)
{
ThrowExceptionForLastWin32Error();
}
}
/// <summary>
/// Add all resources from a source PE file. It is assumed
/// that the input is a valid PE file. If it is not, an
/// exception will be thrown. This will not modify the target
/// until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResourcesFromPEImage(string peFile)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
// Using both flags lets the OS loader decide how to load
// it most efficiently. Either mode will prevent other
// processes from modifying the module while it is loaded.
IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero,
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE |
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (hModule == IntPtr.Zero)
{
ThrowExceptionForLastWin32Error();
}
var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback);
var errorInfo = new EnumResourcesErrorInfo();
GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo);
var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle);
try
{
if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr))
{
if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT)
{
CaptureEnumResourcesErrorInfo(errorInfoPtr);
errorInfo.ThrowException();
}
}
}
finally
{
errorInfoHandle.Free();
if (!Kernel32.FreeLibrary(hModule))
{
ThrowExceptionForLastWin32Error();
}
}
return this;
}
internal static bool IsIntResource(IntPtr lpType)
{
return ((uint)lpType >> 16) == 0;
}
/// <summary>
/// Add a language-neutral integer resource from a byte[] with
/// a particular type and name. This will not modify the
/// target until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
if (!IsIntResource(lpType) || !IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource types");
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
{
ThrowExceptionForLastWin32Error();
}
return this;
}
/// <summary>
/// Add a language-neutral integer resource from a byte[] with
/// a particular type and name. This will not modify the
/// target until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
if (!IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource names");
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
{
ThrowExceptionForLastWin32Error();
}
return this;
}
/// <summary>
/// Write the pending resource updates to the target PE
/// file. After this, the ResourceUpdater no longer maintains
/// an update handle, and can not be used for further updates.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public void Update()
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
try
{
if (!Kernel32.EndUpdateResource(hUpdate, false))
{
ThrowExceptionForLastWin32Error();
}
}
finally
{
hUpdate.SetHandleAsInvalid();
}
}
private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam)
{
var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback);
if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam)
{
var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback);
if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam)
{
IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang);
if (hResource == IntPtr.Zero)
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
// hResourceLoaded is just a handle to the resource, which
// can be used to get the resource data
IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource);
if (hResourceLoaded == IntPtr.Zero)
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
// This doesn't actually lock memory. It just retrieves a
// pointer to the resource data. The pointer is valid
// until the module is unloaded.
IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded);
if (lpResourceData == IntPtr.Zero)
{
((EnumResourcesErrorInfo)GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true;
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource)))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private class EnumResourcesErrorInfo
{
public int hResult;
public bool failedToLockResource;
public void ThrowException()
{
if (failedToLockResource)
{
Debug.Assert(hResult == 0);
throw new ResourceNotAvailableException("Failed to lock resource");
}
Debug.Assert(hResult != 0);
throw new HResultException(hResult);
}
}
private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr)
{
int hResult = Marshal.GetHRForLastWin32Error();
if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT)
{
GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr);
var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target;
errorInfo.hResult = hResult;
}
}
private class ResourceNotAvailableException : Exception
{
public ResourceNotAvailableException(string message) : base(message)
{
}
}
private static void ThrowExceptionForLastWin32Error()
{
throw new HResultException(Marshal.GetHRForLastWin32Error());
}
private static void ThrowExceptionForInvalidUpdate()
{
throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing)
{
if (disposing)
{
hUpdate.Dispose();
}
}
}
}

View File

@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// An instance of this exception is thrown when an AppHost binary update
/// fails due to known user errors.
/// </summary>
public class AppHostUpdateException : Exception
{
internal AppHostUpdateException(string message = null)
: base(message)
{
}
}
/// <summary>
/// The application host executable cannot be customized because adding resources requires
/// that the build be performed on Windows (excluding Nano Server).
/// </summary>
public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
{
internal AppHostCustomizationUnsupportedOSException()
{
}
}
/// <summary>
/// The MachO application host executable cannot be customized because
/// it was not in the expected format
/// </summary>
public sealed class AppHostMachOFormatException : AppHostUpdateException
{
public readonly MachOFormatError Error;
internal AppHostMachOFormatException(MachOFormatError error)
{
Error = error;
}
}
/// <summary>
/// Unable to use the input file as application host executable because it's not a
/// Windows executable for the CUI (Console) subsystem.
/// </summary>
public sealed class AppHostNotCUIException : AppHostUpdateException
{
internal AppHostNotCUIException()
{
}
}
/// <summary>
/// Unable to use the input file as an application host executable
/// because it's not a Windows PE file
/// </summary>
public sealed class AppHostNotPEFileException : AppHostUpdateException
{
internal AppHostNotPEFileException()
{
}
}
/// <summary>
/// Unable to sign the apphost binary.
/// </summary>
public sealed class AppHostSigningException : AppHostUpdateException
{
public readonly int ExitCode;
internal AppHostSigningException(int exitCode, string signingErrorMessage)
: base(signingErrorMessage)
{
}
}
/// <summary>
/// Given app file name is longer than 1024 bytes
/// </summary>
public sealed class AppNameTooLongException : AppHostUpdateException
{
public string LongName { get; }
internal AppNameTooLongException(string name)
{
LongName = name;
}
}
}

View File

@@ -0,0 +1,202 @@
// 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;
namespace Microsoft.NET.HostModel.AppHost
{
public static class BinaryUtils
{
internal static unsafe void SearchAndReplace(
MemoryMappedViewAccessor accessor,
byte[] searchPattern,
byte[] patternToReplace,
bool pad0s = true)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
int position = KMPSearch(searchPattern, bytes, accessor.Capacity);
if (position < 0)
{
throw new PlaceHolderNotFoundInAppHostException(searchPattern);
}
accessor.WriteArray(
position: position,
array: patternToReplace,
offset: 0,
count: patternToReplace.Length);
if (pad0s)
{
Pad0(searchPattern, patternToReplace, bytes, position);
}
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset)
{
if (patternToReplace.Length < searchPattern.Length)
{
for (int i = patternToReplace.Length; i < searchPattern.Length; i++)
{
bytes[i + offset] = 0x0;
}
}
}
public static unsafe void SearchAndReplace(
string filePath,
byte[] searchPattern,
byte[] patternToReplace,
bool pad0s = true)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
SearchAndReplace(accessor, searchPattern, patternToReplace, pad0s);
}
}
}
internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern)
{
var safeBuffer = accessor.SafeMemoryMappedViewHandle;
return KMPSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (int)safeBuffer.ByteLength);
}
public static unsafe int SearchInFile(string filePath, byte[] searchPattern)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
{
return SearchInFile(accessor, searchPattern);
}
}
}
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
private static int[] ComputeKMPFailureFunction(byte[] pattern)
{
int[] table = new int[pattern.Length];
if (pattern.Length >= 1)
{
table[0] = -1;
}
if (pattern.Length >= 2)
{
table[1] = 0;
}
int pos = 2;
int cnd = 0;
while (pos < pattern.Length)
{
if (pattern[pos - 1] == pattern[cnd])
{
table[pos] = cnd + 1;
cnd++;
pos++;
}
else if (cnd > 0)
{
cnd = table[cnd];
}
else
{
table[pos] = 0;
pos++;
}
}
return table;
}
// See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength)
{
int m = 0;
int i = 0;
int[] table = ComputeKMPFailureFunction(pattern);
while (m + i < bytesLength)
{
if (pattern[i] == bytes[m + i])
{
if (i == pattern.Length - 1)
{
return m;
}
i++;
}
else
{
if (table[i] > -1)
{
m = m + i - table[i];
i = table[i];
}
else
{
m++;
i = 0;
}
}
}
return -1;
}
public static void CopyFile(string sourcePath, string destinationPath)
{
var destinationDirectory = new FileInfo(destinationPath).Directory.FullName;
if (!Directory.Exists(destinationDirectory))
{
Directory.CreateDirectory(destinationDirectory);
}
// Copy file to destination path so it inherits the same attributes/permissions.
File.Copy(sourcePath, destinationPath, overwrite: true);
}
internal static void WriteToStream(MemoryMappedViewAccessor sourceViewAccessor, FileStream fileStream, long length)
{
int pos = 0;
int bufSize = 16384; //16K
byte[] buf = new byte[bufSize];
length = Math.Min(length, sourceViewAccessor.Capacity);
do
{
int bytesRequested = Math.Min((int)length - pos, bufSize);
if (bytesRequested <= 0)
{
break;
}
int bytesRead = sourceViewAccessor.ReadArray(pos, buf, 0, bytesRequested);
if (bytesRead > 0)
{
fileStream.Write(buf, 0, bytesRead);
pos += bytesRead;
}
}
while (true);
}
}
}

View File

@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
namespace Microsoft.NET.HostModel.AppHost
{
internal static class ElfUtils
{
// The Linux Headers are copied from elf.h
#pragma warning disable 0649
private struct ElfHeader
{
private byte EI_MAG0;
private byte EI_MAG1;
private byte EI_MAG2;
private byte EI_MAG3;
public bool IsValid()
{
return EI_MAG0 == 0x7f &&
EI_MAG1 == 0x45 &&
EI_MAG2 == 0x4C &&
EI_MAG3 == 0x46;
}
}
#pragma warning restore 0649
public static bool IsElfImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < 16) // EI_NIDENT = 16
{
return false;
}
byte[] eIdent = reader.ReadBytes(4);
// Check that the first four bytes are 0x7f, 'E', 'L', 'F'
return eIdent[0] == 0x7f &&
eIdent[1] == 0x45 &&
eIdent[2] == 0x4C &&
eIdent[3] == 0x46;
}
}
}
}

View File

@@ -0,0 +1,22 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
namespace Microsoft.NET.HostModel
{
/// <summary>
/// Represents an exception thrown because of a Win32 error
/// </summary>
public class HResultException : Exception
{
public readonly int Win32HResult;
public HResultException(int hResult) : base(hResult.ToString("X4"))
{
Win32HResult = hResult;
}
}
}

View File

@@ -0,0 +1,267 @@
// 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// Embeds the App Name into the AppHost.exe
/// If an apphost is a single-file bundle, updates the location of the bundle headers.
/// </summary>
public static class HostWriter
{
/// <summary>
/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored.
/// </summary>
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
/// <summary>
/// Create an AppHost with embedded configuration of app binary location
/// </summary>
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
public static void CreateAppHost(
string appHostSourceFilePath,
string appHostDestinationFilePath,
string appBinaryFilePath,
bool windowsGraphicalUserInterface = false,
string assemblyToCopyResorcesFrom = null,
bool enableMacOSCodeSign = false)
{
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
if (bytesToWrite.Length > 1024)
{
throw new AppNameTooLongException(appBinaryFilePath);
}
bool appHostIsPEImage = false;
void RewriteAppHost(MemoryMappedViewAccessor accessor)
{
// Re-write the destination apphost with the proper contents.
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
appHostIsPEImage = PEUtils.IsPEImage(accessor);
if (windowsGraphicalUserInterface)
{
if (!appHostIsPEImage)
{
throw new AppHostNotPEFileException();
}
PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
}
}
void UpdateResources()
{
if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
{
if (ResourceUpdater.IsSupportedOS())
{
// Copy resources from managed dll to the apphost
new ResourceUpdater(appHostDestinationFilePath)
.AddResourcesFromPEImage(assemblyToCopyResorcesFrom)
.Update();
}
else
{
throw new AppHostCustomizationUnsupportedOSException();
}
}
}
try
{
RetryUtil.RetryOnIOError(() =>
{
FileStream appHostSourceStream = null;
MemoryMappedFile memoryMappedFile = null;
MemoryMappedViewAccessor memoryMappedViewAccessor = null;
try
{
// Open the source host file.
appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
// Get the size of the source app host to ensure that we don't write extra data to the destination.
// On Windows, the size of the view accessor is rounded up to the next page boundary.
long sourceAppHostLength = appHostSourceStream.Length;
// Transform the host file in-memory.
RewriteAppHost(memoryMappedViewAccessor);
// Save the transformed host.
using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
{
BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength);
// Remove the signature from MachO hosts.
if (!appHostIsPEImage)
{
MachOUtils.RemoveSignature(fileStream);
}
}
}
finally
{
memoryMappedViewAccessor?.Dispose();
memoryMappedFile?.Dispose();
appHostSourceStream?.Dispose();
}
});
RetryUtil.RetryOnWin32Error(UpdateResources);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
const int EINTR = 4;
int chmodReturnCode = 0;
do
{
chmodReturnCode = chmod(appHostDestinationFilePath, filePermissionOctal);
}
while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
if (chmodReturnCode == -1)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}.");
}
if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
CodeSign(appHostDestinationFilePath);
}
}
catch (Exception ex)
{
// Delete the destination file so we don't leave an unmodified apphost
try
{
File.Delete(appHostDestinationFilePath);
}
catch (Exception failedToDeleteEx)
{
throw new AggregateException(ex, failedToDeleteEx);
}
throw;
}
}
/// <summary>
/// Set the current AppHost as a single-file bundle.
/// </summary>
/// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
/// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
public static void SetAsBundle(
string appHostPath,
long bundleHeaderOffset)
{
byte[] bundleHeaderPlaceholder = {
// 8 bytes represent the bundle header-offset
// Zero for non-bundle apphosts (default).
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
// Re-write the destination apphost with the proper contents.
RetryUtil.RetryOnIOError(() =>
BinaryUtils.SearchAndReplace(appHostPath,
bundleHeaderPlaceholder,
BitConverter.GetBytes(bundleHeaderOffset),
pad0s: false));
RetryUtil.RetryOnIOError(() =>
MachOUtils.AdjustHeadersForBundle(appHostPath));
// Memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
}
/// <summary>
/// Check if the an AppHost is a single-file bundle
/// </summary>
/// <param name="appHostFilePath">The path of Apphost to check</param>
/// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
/// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
{
byte[] bundleSignature = {
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
long headerOffset = 0;
void FindBundleHeader()
{
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath))
{
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
{
int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
if (position == -1)
{
throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
}
headerOffset = accessor.ReadInt64(position - sizeof(long));
}
}
}
RetryUtil.RetryOnIOError(FindBundleHeader);
bundleHeaderOffset = headerOffset;
return headerOffset != 0;
}
private static void CodeSign(string appHostPath)
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
const string codesign = @"/usr/bin/codesign";
if (!File.Exists(codesign))
return;
var psi = new ProcessStartInfo()
{
Arguments = $"-s - \"{appHostPath}\"",
FileName = codesign,
RedirectStandardError = true,
};
using (var p = Process.Start(psi))
{
p.WaitForExit();
if (p.ExitCode != 0)
throw new AppHostSigningException(p.ExitCode, p.StandardError.ReadToEnd());
}
}
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string pathname, int mode);
}
}

View File

@@ -0,0 +1,26 @@
// 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.AppHost
{
/// <summary>
/// Additional details about the failure with caused an AppHostMachOFormatException
/// </summary>
public enum MachOFormatError
{
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
SignBlobNotLast, // Signature blob must be at the very end of the file
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
MemoryMapAccessFault, // Error reading the memory-mapped apphost
InvalidUTF8, // UTF8 decoding failed
SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle)
}
}

View File

@@ -0,0 +1,446 @@
// 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;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.NET.HostModel.AppHost
{
internal static class MachOUtils
{
// The MachO Headers are copied from
// https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
//
// The data fields and enumerations match the structure definitions in the above file,
// and hence do not conform to C# CoreFx naming style.
private enum Magic : uint
{
MH_MAGIC = 0xfeedface,
MH_CIGAM = 0xcefaedfe,
MH_MAGIC_64 = 0xfeedfacf,
MH_CIGAM_64 = 0xcffaedfe
}
private enum FileType : uint
{
MH_EXECUTE = 0x2
}
#pragma warning disable 0649
private struct MachHeader
{
public Magic magic;
public int cputype;
public int cpusubtype;
public FileType filetype;
public uint ncmds;
public uint sizeofcmds;
public uint flags;
public uint reserved;
public bool Is64BitExecutable()
{
return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
}
public bool IsValid()
{
switch (magic)
{
case Magic.MH_CIGAM:
case Magic.MH_CIGAM_64:
case Magic.MH_MAGIC:
case Magic.MH_MAGIC_64:
return true;
default:
return false;
}
}
}
private enum Command : uint
{
LC_SYMTAB = 0x2,
LC_SEGMENT_64 = 0x19,
LC_CODE_SIGNATURE = 0x1d,
}
private struct LoadCommand
{
public Command cmd;
public uint cmdsize;
}
// The linkedit_data_command contains the offsets and sizes of a blob
// of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
private struct LinkEditDataCommand
{
public Command cmd;
public uint cmdsize;
public uint dataoff;
public uint datasize;
}
private struct SymtabCommand
{
public uint cmd;
public uint cmdsize;
public uint symoff;
public uint nsyms;
public uint stroff;
public uint strsize;
};
private unsafe struct SegmentCommand64
{
public Command cmd;
public uint cmdsize;
public fixed byte segname[16];
public ulong vmaddr;
public ulong vmsize;
public ulong fileoff;
public ulong filesize;
public int maxprot;
public int initprot;
public uint nsects;
public uint flags;
public string SegName
{
get
{
fixed (byte* p = segname)
{
int len = 0;
while (*(p + len) != 0 && len++ < 16) ;
try
{
return Encoding.UTF8.GetString(p, len);
}
catch (ArgumentException)
{
throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
}
}
}
}
}
#pragma warning restore 0649
private static void Verify(bool condition, MachOFormatError error)
{
if (!condition)
{
throw new AppHostMachOFormatException(error);
}
}
public static bool IsMachOImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < 256) // Header size
{
return false;
}
uint magic = reader.ReadUInt32();
return Enum.IsDefined(typeof(Magic), magic);
}
}
/// <summary>
/// This Method is a utility to remove the code-signature (if any)
/// from a MachO AppHost binary.
///
/// The tool assumes the following layout of the executable:
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
/// ...
/// LC_CODE_SIGNATURE (last)
///
/// * ... Different Segments ...
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB
/// * (Some alignment bytes)
/// * The Code-signature
///
/// In order to remove the signature, the method:
/// - Removes (zeros out) the LC_CODE_SIGNATURE command
/// - Adjusts the size and count of the load commands in the header
/// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
/// - Truncates the apphost file to the end of the __LINKEDIT segment
///
/// </summary>
/// <param name="stream">Stream containing the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - It is a signed binary, and
/// - The signature was successfully removed
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
public static unsafe bool RemoveSignature(FileStream stream)
{
uint signatureSize = 0;
using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
mapName: null,
capacity: 0,
MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None,
leaveOpen: true))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
MachHeader* header = (MachHeader*)file;
if (!header->IsValid())
{
// Not a MachO file.
return false;
}
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;
for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
else if (command->cmd == Command.LC_CODE_SIGNATURE)
{
Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
signature = (LinkEditDataCommand*)command;
break;
}
file += command->cmdsize;
}
if (signature != null)
{
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);
var symtabEnd = symtab->stroff + symtab->strsize;
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
var signatureEnd = signature->dataoff + signature->datasize;
var fileEnd = (ulong)stream.Length;
Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);
Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);
// The signature blob immediately follows the symtab blob,
// except for a few bytes of padding.
Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);
// Remove the signature command
header->ncmds--;
header->sizeofcmds -= signature->cmdsize;
Unsafe.InitBlock(signature, 0, signature->cmdsize);
// Remove the signature blob (note for truncation)
signatureSize = (uint)(fileEnd - symtabEnd);
// Adjust the __LINKEDIT segment load command
linkEdit->filesize -= signatureSize;
// codesign --remove-signature doesn't reset the vmsize.
// Setting the vmsize here makes the output bin-equal with the original
// unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
linkEdit->vmsize = linkEdit->filesize;
}
}
finally
{
if (file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}
if (signatureSize != 0)
{
// The signature was removed, update the file length
stream.SetLength(stream.Length - signatureSize);
return true;
}
return false;
}
/// <summary>
/// This Method is a utility to adjust the apphost MachO-header
/// to include the bytes added by the single-file bundler at the end of the file.
///
/// The tool assumes the following layout of the executable
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
///
/// * ... Different Segments
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB (last)
///
/// The MAC codesign tool places several restrictions on the layout
/// * The __LINKEDIT segment must be the last one
/// * The __LINKEDIT segment must cover the end of the file
/// * All bytes in the __LINKEDIT segment are used by other linkage commands
/// (ex: symbol/string table, dynamic load information etc)
///
/// In order to circumvent these restrictions, we:
/// * Extend the __LINKEDIT segment to include the bundle-data
/// * Extend the string table to include all the bundle-data
/// (that is, the bundle-data appear as strings to the loader/codesign tool).
///
/// This method has certain limitations:
/// * The bytes for the bundler may be unnecessarily loaded at startup
/// * Tools that process the string table may be confused (?)
/// * The string table size is limited to 4GB. Bundles larger than that size
/// cannot be accomodated by this utility.
///
/// </summary>
/// <param name="filePath">Path to the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - The additional bytes were successfully accomodated within the MachO segments.
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
public static unsafe bool AdjustHeadersForBundle(string filePath)
{
ulong fileLength = (ulong)new FileInfo(filePath).Length;
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);
MachHeader* header = (MachHeader*)file;
if (!header->IsValid())
{
// Not a MachO file.
return false;
}
Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;
for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
file += command->cmdsize;
}
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);
// Update the string table to include bundle-data
ulong newStringTableSize = fileLength - symtab->stroff;
if (newStringTableSize > uint.MaxValue)
{
// Too big, too bad;
return false;
}
symtab->strsize = (uint)newStringTableSize;
// Update the __LINKEDIT segment to include bundle-data
linkEdit->filesize = fileLength - linkEdit->fileoff;
linkEdit->vmsize = linkEdit->filesize;
}
finally
{
if (file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}
return true;
}
}
}

View File

@@ -0,0 +1,180 @@
// 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;
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 sybsystem 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>
/// <returns>true if the accessor represents a PE image, false otherwise.</returns>
internal static unsafe bool IsPEImage(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// 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))
{
return false;
}
return true;
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static bool IsPEImage(string filePath)
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
if (reader.BaseStream.Length < PEHeaderPointerOffset + sizeof(uint))
{
return false;
}
ushort signature = reader.ReadUInt16();
return signature == PEFileSignature;
}
}
/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
{
throw new AppHostNotPEFileException();
}
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
// 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)
{
throw new AppHostNotCUIException();
}
// Set the subsystem to GUI
subsystem[0] = WindowsGUISubsystem;
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static unsafe void SetWindowsGraphicalUserInterfaceBit(string filePath)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
SetWindowsGraphicalUserInterfaceBit(accessor);
}
}
}
/// <summary>
/// This method will return the subsystem CUI/GUI value. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe ushort GetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
uint peHeaderOffset = ((uint*)(bytes + PEHeaderPointerOffset))[0];
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(ushort))
{
throw new AppHostNotPEFileException();
}
ushort* subsystem = ((ushort*)(bytes + peHeaderOffset + SubsystemOffset));
return subsystem[0];
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
public static unsafe ushort GetWindowsGraphicalUserInterfaceBit(string filePath)
{
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
return GetWindowsGraphicalUserInterfaceBit(accessor);
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// Unable to use input file as a valid application host executable, as it does not contain
/// the expected placeholder byte sequence.
/// </summary>
public class PlaceHolderNotFoundInAppHostException : AppHostUpdateException
{
public byte[] MissingPattern { get; }
public PlaceHolderNotFoundInAppHostException(byte[] pattern)
{
MissingPattern = pattern;
}
}
}

View File

@@ -0,0 +1,98 @@
// 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;
using System.Text;
using System.Threading;
namespace Microsoft.NET.HostModel
{
/// <summary>
/// HostModel library implements several services for updating the AppHost DLL.
/// These updates involve multiple file open/close operations.
/// An Antivirus scanner may intercept in-between and lock the file,
/// causing the operations to fail with IO-Error.
/// So, the operations are retried a few times on failures such as
/// - IOException
/// - Failure with Win32 errors indicating file-lock
/// </summary>
public static class RetryUtil
{
public const int NumberOfRetries = 500;
public const int NumMilliSecondsToWait = 100;
public static void RetryOnIOError(Action func)
{
for (int i = 1; i <= NumberOfRetries; i++)
{
try
{
func();
break;
}
catch (IOException) when (i < NumberOfRetries)
{
Thread.Sleep(NumMilliSecondsToWait);
}
}
}
public static void RetryOnWin32Error(Action func)
{
static bool IsKnownIrrecoverableError(int hresult)
{
// Error codes are defined in winerror.h
// The error code is stored in the lowest 16 bits of the HResult
switch (hresult & 0xffff)
{
case 0x00000001: // ERROR_INVALID_FUNCTION
case 0x00000002: // ERROR_FILE_NOT_FOUND
case 0x00000003: // ERROR_PATH_NOT_FOUND
case 0x00000006: // ERROR_INVALID_HANDLE
case 0x00000008: // ERROR_NOT_ENOUGH_MEMORY
case 0x0000000B: // ERROR_BAD_FORMAT
case 0x0000000E: // ERROR_OUTOFMEMORY
case 0x0000000F: // ERROR_INVALID_DRIVE
case 0x00000012: // ERROR_NO_MORE_FILES
case 0x00000035: // ERROR_BAD_NETPATH
case 0x00000057: // ERROR_INVALID_PARAMETER
case 0x00000071: // ERROR_NO_MORE_SEARCH_HANDLES
case 0x00000072: // ERROR_INVALID_TARGET_HANDLE
case 0x00000078: // ERROR_CALL_NOT_IMPLEMENTED
case 0x0000007B: // ERROR_INVALID_NAME
case 0x0000007C: // ERROR_INVALID_LEVEL
case 0x0000007D: // ERROR_NO_VOLUME_LABEL
case 0x0000009A: // ERROR_LABEL_TOO_LONG
case 0x000000A0: // ERROR_BAD_ARGUMENTS
case 0x000000A1: // ERROR_BAD_PATHNAME
case 0x000000CE: // ERROR_FILENAME_EXCED_RANGE
case 0x000000DF: // ERROR_FILE_TOO_LARGE
case 0x000003ED: // ERROR_UNRECOGNIZED_VOLUME
case 0x000003EE: // ERROR_FILE_INVALID
case 0x00000651: // ERROR_DEVICE_REMOVED
return true;
default:
return false;
}
}
for (int i = 1; i <= NumberOfRetries; i++)
{
try
{
func();
break;
}
catch (HResultException hrex)
when (i < NumberOfRetries && !IsKnownIrrecoverableError(hrex.Win32HResult))
{
Thread.Sleep(NumMilliSecondsToWait);
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// BundleOptions: Optional settings for configuring the type of files
/// included in the single file bundle.
/// </summary>
[Flags]
public enum BundleOptions
{
None = 0,
BundleNativeBinaries = 1,
BundleOtherFiles = 2,
BundleSymbolFiles = 4,
BundleAllContent = BundleNativeBinaries | BundleOtherFiles,
EnableCompression = 8,
};
}

View File

@@ -0,0 +1,342 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using Microsoft.NET.HostModel.AppHost;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Bundler: Functionality to embed the managed app and its dependencies
/// into the host native binary.
/// </summary>
public class Bundler
{
public const uint BundlerMajorVersion = 6;
public const uint BundlerMinorVersion = 0;
private readonly string HostName;
private readonly string OutputDir;
private readonly string DepsJson;
private readonly string RuntimeConfigJson;
private readonly string RuntimeConfigDevJson;
private readonly Trace Tracer;
public readonly Manifest BundleManifest;
private readonly TargetInfo Target;
private readonly BundleOptions Options;
public Bundler(string hostName,
string outputDir,
BundleOptions options = BundleOptions.None,
OSPlatform? targetOS = null,
Architecture? targetArch = null,
Version targetFrameworkVersion = null,
bool diagnosticOutput = false,
string appAssemblyName = null)
{
Tracer = new Trace(diagnosticOutput);
HostName = hostName;
OutputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir);
Target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion);
if (Target.BundleMajorVersion < 6 &&
(options & BundleOptions.EnableCompression) != 0)
{
throw new ArgumentException("Compression requires framework version 6.0 or above", nameof(options));
}
appAssemblyName ??= Target.GetAssemblyName(hostName);
DepsJson = appAssemblyName + ".deps.json";
RuntimeConfigJson = appAssemblyName + ".runtimeconfig.json";
RuntimeConfigDevJson = appAssemblyName + ".runtimeconfig.dev.json";
BundleManifest = new Manifest(Target.BundleMajorVersion, netcoreapp3CompatMode: options.HasFlag(BundleOptions.BundleAllContent));
Options = Target.DefaultOptions | options;
}
private bool ShouldCompress(FileType type)
{
if (!Options.HasFlag(BundleOptions.EnableCompression))
{
return false;
}
switch (type)
{
case FileType.DepsJson:
case FileType.RuntimeConfigJson:
return false;
default:
return true;
}
}
/// <summary>
/// Embed 'file' into 'bundle'
/// </summary>
/// <returns>
/// startOffset: offset of the start 'file' within 'bundle'
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
/// </returns>
private (long startOffset, long compressedSize) AddToBundle(Stream bundle, Stream file, FileType type)
{
long startOffset = bundle.Position;
if (ShouldCompress(type))
{
long fileLength = file.Length;
file.Position = 0;
// We use DeflateStream here.
// It uses GZip algorithm, but with a trivial header that does not contain file info.
using (DeflateStream compressionStream = new DeflateStream(bundle, CompressionLevel.Optimal, leaveOpen: true))
{
file.CopyTo(compressionStream);
}
long compressedSize = bundle.Position - startOffset;
if (compressedSize < fileLength * 0.75)
{
return (startOffset, compressedSize);
}
// compression rate was not good enough
// roll back the bundle offset and let the uncompressed code path take care of the entry.
bundle.Seek(startOffset, SeekOrigin.Begin);
}
if (type == FileType.Assembly)
{
long misalignment = (bundle.Position % Target.AssemblyAlignment);
if (misalignment != 0)
{
long padding = Target.AssemblyAlignment - misalignment;
bundle.Position += padding;
}
}
file.Position = 0;
startOffset = bundle.Position;
file.CopyTo(bundle);
return (startOffset, 0);
}
private bool IsHost(string fileRelativePath)
{
return fileRelativePath.Equals(HostName);
}
private bool ShouldIgnore(string fileRelativePath)
{
return fileRelativePath.Equals(RuntimeConfigDevJson);
}
private bool ShouldExclude(FileType type, string relativePath)
{
switch (type)
{
case FileType.Assembly:
case FileType.DepsJson:
case FileType.RuntimeConfigJson:
return false;
case FileType.NativeBinary:
return !Options.HasFlag(BundleOptions.BundleNativeBinaries) || Target.ShouldExclude(relativePath);
case FileType.Symbols:
return !Options.HasFlag(BundleOptions.BundleSymbolFiles);
case FileType.Unknown:
return !Options.HasFlag(BundleOptions.BundleOtherFiles);
default:
Debug.Assert(false);
return false;
}
}
private bool IsAssembly(string path, out bool isPE)
{
isPE = false;
using (FileStream file = File.OpenRead(path))
{
try
{
PEReader peReader = new PEReader(file);
CorHeader corHeader = peReader.PEHeaders.CorHeader;
isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage
return corHeader != null;
}
catch (BadImageFormatException)
{
}
}
return false;
}
private FileType InferType(FileSpec fileSpec)
{
if (fileSpec.BundleRelativePath.Equals(DepsJson))
{
return FileType.DepsJson;
}
if (fileSpec.BundleRelativePath.Equals(RuntimeConfigJson))
{
return FileType.RuntimeConfigJson;
}
if (Path.GetExtension(fileSpec.BundleRelativePath).ToLowerInvariant().Equals(".pdb"))
{
return FileType.Symbols;
}
bool isPE;
if (IsAssembly(fileSpec.SourcePath, out isPE))
{
return FileType.Assembly;
}
bool isNativeBinary = Target.IsWindows ? isPE : Target.IsNativeBinary(fileSpec.SourcePath);
if (isNativeBinary)
{
return FileType.NativeBinary;
}
return FileType.Unknown;
}
/// <summary>
/// Generate a bundle, given the specification of embedded files
/// </summary>
/// <param name="fileSpecs">
/// An enumeration FileSpecs for the files to be embedded.
///
/// Files in fileSpecs that are not bundled within the single file bundle,
/// and should be published as separate files are marked as "IsExcluded" by this method.
/// This doesn't include unbundled files that should be dropped, and not publised as output.
/// </param>
/// <returns>
/// The full path the the generated bundle file
/// </returns>
/// <exceptions>
/// ArgumentException if input is invalid
/// IOExceptions and ArgumentExceptions from callees flow to the caller.
/// </exceptions>
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
{
Tracer.Log($"Bundler Version: {BundlerMajorVersion}.{BundlerMinorVersion}");
Tracer.Log($"Bundle Version: {BundleManifest.BundleVersion}");
Tracer.Log($"Target Runtime: {Target}");
Tracer.Log($"Bundler Options: {Options}");
if (fileSpecs.Any(x => !x.IsValid()))
{
throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
}
string hostSource;
try
{
hostSource = fileSpecs.Where(x => x.BundleRelativePath.Equals(HostName)).Single().SourcePath;
}
catch (InvalidOperationException)
{
throw new ArgumentException("Invalid input specification: Must specify the host binary");
}
string bundlePath = Path.Combine(OutputDir, HostName);
if (File.Exists(bundlePath))
{
Tracer.Log($"Ovewriting existing File {bundlePath}");
}
BinaryUtils.CopyFile(hostSource, bundlePath);
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
long headerOffset = 0;
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
{
Stream bundle = writer.BaseStream;
bundle.Position = bundle.Length;
foreach (var fileSpec in fileSpecs)
{
string relativePath = fileSpec.BundleRelativePath;
if (IsHost(relativePath))
{
continue;
}
if (ShouldIgnore(relativePath))
{
Tracer.Log($"Ignore: {relativePath}");
continue;
}
FileType type = InferType(fileSpec);
if (ShouldExclude(type, relativePath))
{
Tracer.Log($"Exclude [{type}]: {relativePath}");
fileSpec.Excluded = true;
continue;
}
if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
{
if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
{
throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
}
// Exact duplicate - intentionally skip and don't include a second copy in the bundle
continue;
}
else
{
relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
}
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
{
FileType targetType = Target.TargetSpecificFileType(type);
(long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion);
Tracer.Log($"Embed: {entry}");
}
}
// Write the bundle manifest
headerOffset = BundleManifest.Write(writer);
Tracer.Log($"Header Offset={headerOffset}");
Tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
Tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}");
}
HostWriter.SetAsBundle(bundlePath, headerOffset);
return bundlePath;
}
}
}

View File

@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// FileEntry: Records information about embedded files.
///
/// The bundle manifest records the following meta-data for each
/// file embedded in the bundle:
/// * Type (1 byte)
/// * NameLength (7-bit extension encoding, typically 1 byte)
/// * Name ("NameLength" Bytes)
/// * Offset (Int64)
/// * Size (Int64)
/// === present only in bundle version 3+
/// * CompressedSize (Int64) 0 indicates No Compression
/// </summary>
public class FileEntry
{
public readonly uint BundleMajorVersion;
public readonly long Offset;
public readonly long Size;
public readonly long CompressedSize;
public readonly FileType Type;
public readonly string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
public const char DirectorySeparatorChar = '/';
public FileEntry(FileType fileType, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion)
{
BundleMajorVersion = bundleMajorVersion;
Type = fileType;
RelativePath = relativePath.Replace('\\', DirectorySeparatorChar);
Offset = offset;
Size = size;
CompressedSize = compressedSize;
}
public void Write(BinaryWriter writer)
{
writer.Write(Offset);
writer.Write(Size);
// compression is used only in version 6.0+
if (BundleMajorVersion >= 6)
{
writer.Write(CompressedSize);
}
writer.Write((byte)Type);
writer.Write(RelativePath);
}
public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}";
}
}

View File

@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Information about files to embed into the Bundle (input to the Bundler).
///
/// SourcePath: path to the file to be bundled at compile time
/// BundleRelativePath: path where the file is expected at run time,
/// relative to the app DLL.
/// </summary>
public class FileSpec
{
public readonly string SourcePath;
public readonly string BundleRelativePath;
public bool Excluded;
public FileSpec(string sourcePath, string bundleRelativePath)
{
SourcePath = sourcePath;
BundleRelativePath = bundleRelativePath;
Excluded = false;
}
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(SourcePath) &&
!string.IsNullOrWhiteSpace(BundleRelativePath);
}
public override string ToString() => $"SourcePath: {SourcePath}, RelativePath: {BundleRelativePath} {(Excluded ? "[Excluded]" : "")}";
}
}

View File

@@ -0,0 +1,21 @@
// 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.Bundle
{
/// <summary>
/// FileType: Identifies the type of file embedded into the bundle.
///
/// The bundler differentiates a few kinds of files via the manifest,
/// with respect to the way in which they'll be used by the runtime.
/// </summary>
public enum FileType : byte
{
Unknown, // Type not determined.
Assembly, // IL and R2R Assemblies
NativeBinary, // NativeBinaries
DepsJson, // .deps.json configuration file
RuntimeConfigJson, // .runtimeconfig.json configuration file
Symbols // PDB Files
};
}

View File

@@ -0,0 +1,177 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// BundleManifest is a description of the contents of a bundle file.
/// This class handles creation and consumption of bundle-manifests.
///
/// Here is the description of the Bundle Layout:
/// _______________________________________________
/// AppHost
///
///
/// ------------Embedded Files ---------------------
/// The embedded files including the app, its
/// configuration files, dependencies, and
/// possibly the runtime.
///
///
///
///
///
///
///
/// ------------ Bundle Header -------------
/// MajorVersion
/// MinorVersion
/// NumEmbeddedFiles
/// ExtractionID
/// DepsJson Location [Version 2+]
/// Offset
/// Size
/// RuntimeConfigJson Location [Version 2+]
/// Offset
/// Size
/// Flags [Version 2+]
/// - - - - - - Manifest Entries - - - - - - - - - - -
/// Series of FileEntries (for each embedded file)
/// [File Type, Name, Offset, Size information]
///
///
///
/// _________________________________________________
/// </summary>
public class Manifest
{
// NetcoreApp3CompatMode flag is set on a .net5 app,
// which chooses to build single-file apps in .netcore3.x compat mode,
// by constructing the bundler with BundleAllConent option.
// This mode is expected to be deprecated in future versions of .NET.
[Flags]
private enum HeaderFlags : ulong
{
None = 0,
NetcoreApp3CompatMode = 1
}
// Bundle ID is a string that is used to uniquely
// identify this bundle. It is choosen to be compatible
// with path-names so that the AppHost can use it in
// extraction path.
public string BundleID { get; private set; }
//Same as Path.GetRandomFileName
private const int BundleIdLength = 12;
private SHA256 bundleHash = SHA256.Create();
public readonly uint BundleMajorVersion;
// The Minor version is currently unused, and is always zero
public const uint BundleMinorVersion = 0;
private FileEntry DepsJsonEntry;
private FileEntry RuntimeConfigJsonEntry;
private HeaderFlags Flags;
public List<FileEntry> Files;
public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";
public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
{
BundleMajorVersion = bundleMajorVersion;
Files = new List<FileEntry>();
Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
}
public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion)
{
if (bundleHash == null)
{
throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained.");
}
FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion);
Files.Add(entry);
fileContent.Position = 0;
byte[] hashBytes = ComputeSha256Hash(fileContent);
bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0);
switch (entry.Type)
{
case FileType.DepsJson:
DepsJsonEntry = entry;
break;
case FileType.RuntimeConfigJson:
RuntimeConfigJsonEntry = entry;
break;
case FileType.Assembly:
break;
default:
break;
}
return entry;
}
private static byte[] ComputeSha256Hash(Stream stream)
{
using (SHA256 sha = SHA256.Create())
{
return sha.ComputeHash(stream);
}
}
private string GenerateDeterministicId()
{
bundleHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
byte[] manifestHash = bundleHash.Hash;
bundleHash.Dispose();
bundleHash = null;
return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_');
}
public long Write(BinaryWriter writer)
{
BundleID = BundleID ?? GenerateDeterministicId();
long startOffset = writer.BaseStream.Position;
// Write the bundle header
writer.Write(BundleMajorVersion);
writer.Write(BundleMinorVersion);
writer.Write(Files.Count);
writer.Write(BundleID);
if (BundleMajorVersion >= 2)
{
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0);
writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0);
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0);
writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0);
writer.Write((ulong)Flags);
}
// Write the manifest entries
foreach (FileEntry entry in Files)
{
entry.Write(writer);
}
return startOffset;
}
public bool Contains(string relativePath)
{
return Files.Any(entry => relativePath.Equals(entry.RelativePath));
}
}
}

View File

@@ -0,0 +1,120 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.NET.HostModel.AppHost;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// TargetInfo: Information about the target for which the single-file bundle is built.
///
/// Currently the TargetInfo only tracks:
/// - the target operating system
/// - the target architecture
/// - the target framework
/// - the default options for this target
/// - the assembly alignment for this target
/// </summary>
public class TargetInfo
{
public readonly OSPlatform OS;
public readonly Architecture Arch;
public readonly Version FrameworkVersion;
public readonly uint BundleMajorVersion;
public readonly BundleOptions DefaultOptions;
public readonly int AssemblyAlignment;
public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVersion)
{
OS = os ?? HostOS;
Arch = arch ?? RuntimeInformation.OSArchitecture;
FrameworkVersion = targetFrameworkVersion ?? net60;
Debug.Assert(IsLinux || IsOSX || IsWindows);
if (FrameworkVersion.CompareTo(net60) >= 0)
{
BundleMajorVersion = 6u;
DefaultOptions = BundleOptions.None;
}
else if (FrameworkVersion.CompareTo(net50) >= 0)
{
BundleMajorVersion = 2u;
DefaultOptions = BundleOptions.None;
}
else if (FrameworkVersion.Major == 3 && (FrameworkVersion.Minor == 0 || FrameworkVersion.Minor == 1))
{
BundleMajorVersion = 1u;
DefaultOptions = BundleOptions.BundleAllContent;
}
else
{
throw new ArgumentException($"Invalid input: Unsupported Target Framework Version {targetFrameworkVersion}");
}
if (IsLinux && Arch == Architecture.Arm64)
{
// We align assemblies in the bundle at 4K so that we can use mmap on Linux without changing the page alignment of ARM64 R2R code.
// This is only necessary for R2R assemblies, but we do it for all assemblies for simplicity.
// See https://github.com/dotnet/runtime/issues/41832.
AssemblyAlignment = 4096;
}
else
{
// Otherwise, assemblies are 16 bytes aligned, so that their sections can be memory-mapped cache aligned.
AssemblyAlignment = 16;
}
}
public bool IsNativeBinary(string filePath)
{
return IsLinux ? ElfUtils.IsElfImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : PEUtils.IsPEImage(filePath);
}
public string GetAssemblyName(string hostName)
{
// This logic to calculate assembly name from hostName should be removed (and probably moved to test helpers)
// once the SDK in the correct assembly name.
return (IsWindows ? Path.GetFileNameWithoutExtension(hostName) : hostName);
}
public override string ToString()
{
string os = IsWindows ? "win" : IsLinux ? "linux" : "osx";
string arch = Arch.ToString().ToLowerInvariant();
return $"OS: {os} Arch: {arch} FrameworkVersion: {FrameworkVersion}";
}
private static OSPlatform HostOS => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OSPlatform.Linux :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OSPlatform.OSX : OSPlatform.Windows;
public bool IsLinux => OS.Equals(OSPlatform.Linux);
public bool IsOSX => OS.Equals(OSPlatform.OSX);
public bool IsWindows => OS.Equals(OSPlatform.Windows);
// The .net core 3 apphost doesn't care about semantics of FileType -- all files are extracted at startup.
// However, the apphost checks that the FileType value is within expected bounds, so set it to the first enumeration.
public FileType TargetSpecificFileType(FileType fileType) => (BundleMajorVersion == 1) ? FileType.Unknown : fileType;
// In .net core 3.x, bundle processing happens within the AppHost.
// Therefore HostFxr and HostPolicy can be bundled within the single-file app.
// In .net 5, bundle processing happens in HostFxr and HostPolicy libraries.
// Therefore, these libraries themselves cannot be bundled into the single-file app.
// This problem is mitigated by statically linking these host components with the AppHost.
// https://github.com/dotnet/runtime/issues/32823
public bool ShouldExclude(string relativePath) =>
(FrameworkVersion.Major != 3) && (relativePath.Equals(HostFxr) || relativePath.Equals(HostPolicy));
private readonly Version net60 = new Version(6, 0);
private readonly Version net50 = new Version(5, 0);
private string HostFxr => IsWindows ? "hostfxr.dll" : IsLinux ? "libhostfxr.so" : "libhostfxr.dylib";
private string HostPolicy => IsWindows ? "hostpolicy.dll" : IsLinux ? "libhostpolicy.so" : "libhostpolicy.dylib";
}
}

View File

@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.NET.HostModel.Bundle
{
/// <summary>
/// Tracing utilities for diagnostic output
/// </summary>
public class Trace
{
private readonly bool Verbose;
public Trace(bool verbose)
{
Verbose = verbose;
}
public void Log(string fmt, params object[] args)
{
if (Verbose)
{
Console.WriteLine("LOG: " + fmt, args);
}
}
public void Error(string type, string message)
{
Console.Error.WriteLine($"ERROR: {message}");
}
}
}

View File

@@ -0,0 +1,4 @@
# HostModel
The code in this folder is taken from dotnet/runtime which is MIT licensed.
https://github.com/dotnet/runtime/tree/main/src/installer/managed/Microsoft.NET.HostModel

View File

@@ -0,0 +1,491 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
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.
/// </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;
///<summary>
/// Determines if the ResourceUpdater is supported by the current operating system.
/// Some versions of Windows, such as Nano Server, do not support the needed APIs.
/// </summary>
public static bool IsSupportedOS()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
try
{
// On Nano Server 1709+, `BeginUpdateResource` is exported but returns a null handle with a zero error
// Try to call `BeginUpdateResource` with an invalid parameter; the error should be non-zero if supported
// 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.
/// </summary>
public ResourceUpdater(string peFile)
{
hUpdate = Kernel32.BeginUpdateResource(peFile, false);
if (hUpdate.IsInvalid)
{
ThrowExceptionForLastWin32Error();
}
}
/// <summary>
/// Add all resources from a source PE file. It is assumed
/// that the input is a valid PE file. If it is not, an
/// exception will be thrown. This will not modify the target
/// until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResourcesFromPEImage(string peFile)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
// Using both flags lets the OS loader decide how to load
// it most efficiently. Either mode will prevent other
// processes from modifying the module while it is loaded.
IntPtr hModule = Kernel32.LoadLibraryEx(peFile, IntPtr.Zero,
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE |
Kernel32.LoadLibraryFlags.LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (hModule == IntPtr.Zero)
{
ThrowExceptionForLastWin32Error();
}
var enumTypesCallback = new Kernel32.EnumResTypeProc(EnumAndUpdateTypesCallback);
var errorInfo = new EnumResourcesErrorInfo();
GCHandle errorInfoHandle = GCHandle.Alloc(errorInfo);
var errorInfoPtr = GCHandle.ToIntPtr(errorInfoHandle);
try
{
if (!Kernel32.EnumResourceTypes(hModule, enumTypesCallback, errorInfoPtr))
{
if (Marshal.GetHRForLastWin32Error() != Kernel32.ResourceDataNotFoundHRESULT)
{
CaptureEnumResourcesErrorInfo(errorInfoPtr);
errorInfo.ThrowException();
}
}
}
finally
{
errorInfoHandle.Free();
if (!Kernel32.FreeLibrary(hModule))
{
ThrowExceptionForLastWin32Error();
}
}
return this;
}
internal static bool IsIntResource(IntPtr lpType)
{
return ((uint)lpType >> 16) == 0;
}
/// <summary>
/// Add a language-neutral integer resource from a byte[] with
/// a particular type and name. This will not modify the
/// target until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
if (!IsIntResource(lpType) || !IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource types");
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
{
ThrowExceptionForLastWin32Error();
}
return this;
}
/// <summary>
/// Add a language-neutral integer resource from a byte[] with
/// a particular type and name. This will not modify the
/// target until Update() is called.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
if (!IsIntResource(lpName))
{
throw new ArgumentException("AddResource can only be used with integer resource names");
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
{
ThrowExceptionForLastWin32Error();
}
return this;
}
/// <summary>
/// Write the pending resource updates to the target PE
/// file. After this, the ResourceUpdater no longer maintains
/// an update handle, and can not be used for further updates.
/// Throws an InvalidOperationException if Update() was already called.
/// </summary>
public void Update()
{
if (hUpdate.IsInvalid)
{
ThrowExceptionForInvalidUpdate();
}
try
{
if (!Kernel32.EndUpdateResource(hUpdate, false))
{
ThrowExceptionForLastWin32Error();
}
}
finally
{
hUpdate.SetHandleAsInvalid();
}
}
private bool EnumAndUpdateTypesCallback(IntPtr hModule, IntPtr lpType, IntPtr lParam)
{
var enumNamesCallback = new Kernel32.EnumResNameProc(EnumAndUpdateNamesCallback);
if (!Kernel32.EnumResourceNames(hModule, lpType, enumNamesCallback, lParam))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private bool EnumAndUpdateNamesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, IntPtr lParam)
{
var enumLanguagesCallback = new Kernel32.EnumResLangProc(EnumAndUpdateLanguagesCallback);
if (!Kernel32.EnumResourceLanguages(hModule, lpType, lpName, enumLanguagesCallback, lParam))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private bool EnumAndUpdateLanguagesCallback(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLang, IntPtr lParam)
{
IntPtr hResource = Kernel32.FindResourceEx(hModule, lpType, lpName, wLang);
if (hResource == IntPtr.Zero)
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
// hResourceLoaded is just a handle to the resource, which
// can be used to get the resource data
IntPtr hResourceLoaded = Kernel32.LoadResource(hModule, hResource);
if (hResourceLoaded == IntPtr.Zero)
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
// This doesn't actually lock memory. It just retrieves a
// pointer to the resource data. The pointer is valid
// until the module is unloaded.
IntPtr lpResourceData = Kernel32.LockResource(hResourceLoaded);
if (lpResourceData == IntPtr.Zero)
{
((EnumResourcesErrorInfo)GCHandle.FromIntPtr(lParam).Target).failedToLockResource = true;
}
if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, wLang, lpResourceData, Kernel32.SizeofResource(hModule, hResource)))
{
CaptureEnumResourcesErrorInfo(lParam);
return false;
}
return true;
}
private class EnumResourcesErrorInfo
{
public int hResult;
public bool failedToLockResource;
public void ThrowException()
{
if (failedToLockResource)
{
Debug.Assert(hResult == 0);
throw new ResourceNotAvailableException("Failed to lock resource");
}
Debug.Assert(hResult != 0);
throw new HResultException(hResult);
}
}
private static void CaptureEnumResourcesErrorInfo(IntPtr errorInfoPtr)
{
int hResult = Marshal.GetHRForLastWin32Error();
if (hResult != Kernel32.UserStoppedResourceEnumerationHRESULT)
{
GCHandle errorInfoHandle = GCHandle.FromIntPtr(errorInfoPtr);
var errorInfo = (EnumResourcesErrorInfo)errorInfoHandle.Target;
errorInfo.hResult = hResult;
}
}
private class ResourceNotAvailableException : Exception
{
public ResourceNotAvailableException(string message) : base(message)
{
}
}
private static void ThrowExceptionForLastWin32Error()
{
throw new HResultException(Marshal.GetHRForLastWin32Error());
}
private static void ThrowExceptionForInvalidUpdate()
{
throw new InvalidOperationException("Update handle is invalid. This instance may not be used for further updates");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing)
{
if (disposing)
{
hUpdate.Dispose();
}
}
}
}

View File

@@ -26,8 +26,4 @@
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="..\..\vendor\Microsoft.NET.HostModel.dll" />
</ItemGroup>
</Project>

Binary file not shown.

4
vendor/README.md vendored
View File

@@ -1,11 +1,11 @@
# Vendor Binaries
This folder contains pre-compiled binaries from a variety of sources. These should be updated periodically.
### Microsoft.NET.HostModel.dll
<!-- ### Microsoft.NET.HostModel.dll
- This is a .NET SDK 6.0.100 binary.
- It's purpose is to allow us to re-pack the `Update.exe` single file bundle with a new exe icon.
- Can be found in the dotnet SDK at "C:\Program Files\dotnet\sdk\6.0.100\Microsoft.NET.HostModel.dll".
- MIT License: https://github.com/dotnet/runtime/blob/main/LICENSE.TXT
- MIT License: https://github.com/dotnet/runtime/blob/main/LICENSE.TXT -->
### singlefilehost.exe
- This is the native exe that has the .net native runtime linked in.