From 88983d39e544f9a3b0e4d408e9ce3c6192a23286 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 27 Jun 2020 16:01:46 -0700 Subject: [PATCH] Remove use of code that involves reflection This allows over 40% of the CoreRT native binary size to be removed by removing reflection capabilities. The "--noreflection" option for the build script can be used to build hactoolnet with no reflection. The Linux build won't always work because creating a new thread for the progress bar runs into some issue with EventSource being removed. --- build/Build.cs | 10 ++- src/LibHac/Keyset.cs | 36 ++++---- src/LibHac/Util.cs | 27 ++++-- src/hactoolnet/HomeFolder.cs | 142 +++++++++++++++++++++++++++++++ src/hactoolnet/Program.cs | 7 ++ src/hactoolnet/hactoolnet.csproj | 3 +- 6 files changed, 196 insertions(+), 29 deletions(-) create mode 100644 src/hactoolnet/HomeFolder.cs diff --git a/build/Build.cs b/build/Build.cs index bc348136..42be3f9b 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -34,6 +34,9 @@ namespace LibHacBuild [Parameter("Don't enable any size-reducing settings on native builds.")] public readonly bool Untrimmed; + [Parameter("Disable reflection in native builds.")] + public readonly bool NoReflection; + [Solution("LibHac.sln")] readonly Solution _solution; AbsolutePath SourceDirectory => RootDirectory / "src"; @@ -358,6 +361,11 @@ namespace LibHacBuild { string buildType = Untrimmed ? "native-untrimmed" : "native"; + if (NoReflection) + { + buildType = "native-noreflection"; + } + DotNetPublishSettings publishSettings = new DotNetPublishSettings() .SetConfiguration(Configuration) .SetProject(HactoolnetProject) @@ -368,7 +376,7 @@ namespace LibHacBuild DotNetPublish(publishSettings); - if (EnvironmentInfo.IsUnix && !Untrimmed) + if (EnvironmentInfo.IsUnix && !Untrimmed && !NoReflection) { File.Copy(CliNativeExe, CliNativeExe + "_unstripped", true); ProcessTasks.StartProcess("strip", CliNativeExe).AssertZeroExitCode(); diff --git a/src/LibHac/Keyset.cs b/src/LibHac/Keyset.cs index a818d6b6..fedeab4a 100644 --- a/src/LibHac/Keyset.cs +++ b/src/LibHac/Keyset.cs @@ -20,19 +20,19 @@ namespace LibHac private const int SdCardKeyIdCount = 3; - public byte[][] KeyblobKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] KeyblobMacKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] EncryptedKeyblobs { get; } = Util.CreateJaggedArray(0x20, 0xB0); - public byte[][] Keyblobs { get; } = Util.CreateJaggedArray(0x20, 0x90); - public byte[][] KeyblobKeySources { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] KeyblobKeys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] KeyblobMacKeys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] EncryptedKeyblobs { get; } = Util.CreateJaggedByteArray(0x20, 0xB0); + public byte[][] Keyblobs { get; } = Util.CreateJaggedByteArray(0x20, 0x90); + public byte[][] KeyblobKeySources { get; } = Util.CreateJaggedByteArray(0x20, 0x10); public byte[] KeyblobMacKeySource { get; } = new byte[0x10]; - public byte[][] TsecRootKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] MasterKekSources { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] MasterKeks { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] TsecRootKeys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] MasterKekSources { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] MasterKeks { get; } = Util.CreateJaggedByteArray(0x20, 0x10); public byte[] MasterKeySource { get; } = new byte[0x10]; - public byte[][] MasterKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] Package1Keys { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][] Package2Keys { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] MasterKeys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] Package1Keys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][] Package2Keys { get; } = Util.CreateJaggedByteArray(0x20, 0x10); public byte[] Package2KeySource { get; } = new byte[0x10]; public byte[] AesKekGenerationSource { get; } = new byte[0x10]; public byte[] AesKeyGenerationSource { get; } = new byte[0x10]; @@ -46,29 +46,29 @@ namespace LibHac public byte[] TitleKekSource { get; } = new byte[0x10]; public byte[] HeaderKekSource { get; } = new byte[0x10]; public byte[] SdCardKekSource { get; } = new byte[0x10]; - public byte[][] SdCardKeySources { get; } = Util.CreateJaggedArray(SdCardKeyIdCount, 0x20); + public byte[][] SdCardKeySources { get; } = Util.CreateJaggedByteArray(SdCardKeyIdCount, 0x20); public byte[] HeaderKeySource { get; } = new byte[0x20]; public byte[] HeaderKey { get; } = new byte[0x20]; public byte[] XciHeaderKey { get; } = new byte[0x10]; - public byte[][] TitleKeks { get; } = Util.CreateJaggedArray(0x20, 0x10); - public byte[][][] KeyAreaKeys { get; } = Util.CreateJaggedArray(0x20, 3, 0x10); + public byte[][] TitleKeks { get; } = Util.CreateJaggedByteArray(0x20, 0x10); + public byte[][][] KeyAreaKeys { get; } = Util.CreateJaggedByteArray(0x20, 3, 0x10); public byte[] EticketRsaKek { get; } = new byte[0x10]; public byte[] RetailSpecificAesKeySource { get; } = new byte[0x10]; public byte[] PerConsoleKeySource { get; } = new byte[0x10]; public byte[] BisKekSource { get; } = new byte[0x10]; - public byte[][] BisKeySource { get; } = Util.CreateJaggedArray(4, 0x20); + public byte[][] BisKeySource { get; } = Util.CreateJaggedByteArray(4, 0x20); public byte[] SslRsaKek { get; } = new byte[0x10]; // Device-specific keys public byte[] SecureBootKey { get; } = new byte[0x10]; public byte[] TsecKey { get; } = new byte[0x10]; public byte[] DeviceKey { get; } = new byte[0x10]; - public byte[][] BisKeys { get; } = Util.CreateJaggedArray(4, 0x20); + public byte[][] BisKeys { get; } = Util.CreateJaggedByteArray(4, 0x20); public byte[] SaveMacKey { get; } = new byte[0x10]; public byte[] SaveMacSdCardKey { get; } = new byte[0x10]; public byte[] SdSeed { get; } = new byte[0x10]; - public byte[][] SdCardKeySourcesSpecific { get; } = Util.CreateJaggedArray(SdCardKeyIdCount, 0x20); - public byte[][] SdCardKeys { get; } = Util.CreateJaggedArray(SdCardKeyIdCount, 0x20); + public byte[][] SdCardKeySourcesSpecific { get; } = Util.CreateJaggedByteArray(SdCardKeyIdCount, 0x20); + public byte[][] SdCardKeys { get; } = Util.CreateJaggedByteArray(SdCardKeyIdCount, 0x20); public RSAParameters EticketExtKeyRsa { get; set; } diff --git a/src/LibHac/Util.cs b/src/LibHac/Util.cs index 2bfd26bb..3ac39ed1 100644 --- a/src/LibHac/Util.cs +++ b/src/LibHac/Util.cs @@ -11,21 +11,30 @@ namespace LibHac { private const int MediaSize = 0x200; - public static T CreateJaggedArray(params int[] lengths) + public static byte[][] CreateJaggedByteArray(int len1, int len2) { - return (T)InitializeJaggedArray(typeof(T).GetElementType(), 0, lengths); + var array = new byte[len1][]; + + for (int i = 0; i < array.Length; i++) + { + array[i] = new byte[len2]; + } + + return array; } - private static object InitializeJaggedArray(Type type, int index, int[] lengths) + public static byte[][][] CreateJaggedByteArray(int len1, int len2, int len3) { - var array = Array.CreateInstance(type, lengths[index]); + var array = new byte[len1][][]; - Type elementType = type.GetElementType(); - if (elementType == null) return array; - - for (int i = 0; i < lengths[index]; i++) + for (int i = 0; i < array.Length; i++) { - array.SetValue(InitializeJaggedArray(elementType, index + 1, lengths), i); + array[i] = new byte[len2][]; + + for (int j = 0; j < array[i].Length; j++) + { + array[i][j] = new byte[len3]; + } } return array; diff --git a/src/hactoolnet/HomeFolder.cs b/src/hactoolnet/HomeFolder.cs new file mode 100644 index 00000000..0270aa70 --- /dev/null +++ b/src/hactoolnet/HomeFolder.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Environment.GetFolderPath calls Enum.IsDefined which currently doesn't work under CoreRT (2020-06-27) +// This code is copied from the .NET runtime with modifications to avoid that. +// The downside is that it won't work in Linux unless the HOME environmental variable is set. + +#if CORERT_NO_REFLECTION +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace hactoolnet +{ + internal static class HomeFolder + { + public static string GetFolderPath(Environment.SpecialFolder folder) => + GetFolderPath(folder, Environment.SpecialFolderOption.None); + + public static string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return GetFolderPathCoreWin(folder, option); + } + else + { + return GetFolderPathCoreWithoutValidation(folder); + } + } + + /// + /// (CSIDL_PROFILE) The root users profile folder "%USERPROFILE%" + /// ("%SystemDrive%\Users\%USERNAME%") + /// + internal const string Profile = "{5E6C858F-0E22-4760-9AFE-EA3317B67173}"; + + private static string GetFolderPathCoreWin(Environment.SpecialFolder folder, + Environment.SpecialFolderOption option) + { + // We're using SHGetKnownFolderPath instead of SHGetFolderPath as SHGetFolderPath is + // capped at MAX_PATH. + // + // Because we validate both of the input enums we shouldn't have to care about CSIDL and flag + // definitions we haven't mapped. If we remove or loosen the checks we'd have to account + // for mapping here (this includes tweaking as SHGetFolderPath would do). + // + // The only SpecialFolderOption defines we have are equivalent to KnownFolderFlags. + + string folderGuid; + + switch (folder) + { + case Environment.SpecialFolder.UserProfile: + folderGuid = Profile; + break; + default: + throw new NotSupportedException(); + } + + return GetKnownFolderPath(folderGuid, option); + } + + private static string GetKnownFolderPath(string folderGuid, Environment.SpecialFolderOption option) + { + var folderId = new Guid(folderGuid); + + int hr = Shell32.SHGetKnownFolderPath(folderId, (uint)option, IntPtr.Zero, out string path); + if (hr != 0) // Not S_OK + { + return string.Empty; + } + + return path; + } + + private static string GetFolderPathCoreWithoutValidation(Environment.SpecialFolder folder) + { + // All other paths are based on the XDG Base Directory Specification: + // https://specifications.freedesktop.org/basedir-spec/latest/ + string home = null; + try + { + home = GetHomeDirectory(); + } + catch (Exception exc) + { + Debug.Fail($"Unable to get home directory: {exc}"); + } + + // Fall back to '/' when we can't determine the home directory. + // This location isn't writable by non-root users which provides some safeguard + // that the application doesn't write data which is meant to be private. + if (string.IsNullOrEmpty(home)) + { + home = "/"; + } + + // TODO: Consider caching (or precomputing and caching) all subsequent results. + // This would significantly improve performance for repeated access, at the expense + // of not being responsive to changes in the underlying environment variables, + // configuration files, etc. + + switch (folder) + { + case Environment.SpecialFolder.UserProfile: + case Environment.SpecialFolder.MyDocuments: // same value as Personal + return home; + } + + // No known path for the SpecialFolder + return string.Empty; + } + + /// Gets the current user's home directory. + /// The path to the home directory, or null if it could not be determined. + internal static string GetHomeDirectory() + { + // First try to get the user's home directory from the HOME environment variable. + // This should work in most cases. + string userHomeDirectory = Environment.GetEnvironmentVariable("HOME"); + if (!string.IsNullOrEmpty(userHomeDirectory)) + return userHomeDirectory; + + throw new NotSupportedException( + "Unable to get your home directory. Please report this on the LibHac GitHub repository." + + "You can use the netcore build in the GitHub repo's releases for now."); + } + } + + internal class Shell32 + { + [DllImport("shell32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)] + internal static extern int SHGetKnownFolderPath( + [MarshalAs(UnmanagedType.LPStruct)] Guid rfid, + uint dwFlags, + IntPtr hToken, + out string ppszPath); + } +} +#endif \ No newline at end of file diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index f2b4ab8a..0302c5b3 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -37,7 +37,10 @@ namespace hactoolnet Console.Error.WriteLine($"\nERROR: {ex.Message}\n"); Console.Error.WriteLine("Additional information:"); +#if !CORERT_NO_REFLECTION Console.Error.WriteLine(ex.GetType().FullName); +#endif + Console.Error.WriteLine(ex.StackTrace); } @@ -167,7 +170,11 @@ namespace hactoolnet { string keyFileName = ctx.Options.UseDevKeys ? "dev.keys" : "prod.keys"; +#if CORERT_NO_REFLECTION + string home = HomeFolder.GetFolderPath(Environment.SpecialFolder.UserProfile); +#else string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); +#endif string homeKeyFile = Path.Combine(home, ".switch", keyFileName); string homeTitleKeyFile = Path.Combine(home, ".switch", "title.keys"); string homeConsoleKeyFile = Path.Combine(home, ".switch", "console.keys"); diff --git a/src/hactoolnet/hactoolnet.csproj b/src/hactoolnet/hactoolnet.csproj index 7f1cabd0..ced02f3f 100644 --- a/src/hactoolnet/hactoolnet.csproj +++ b/src/hactoolnet/hactoolnet.csproj @@ -36,9 +36,10 @@ - + true + CORERT_NO_REFLECTION;$(DefineConstants)