diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 9536079e..10e9eeb6 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -306,3 +306,10 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 428,2,,NullArgument, 428,3,,ArgumentOutOfRange, 428,4,,BufferTooSmall, + +428,1000,1999,InvalidData, +428,1001,1019,InvalidKip, +428,1002,,InvalidKipFileSize,The size of the KIP file was smaller than expected. +428,1003,,InvalidKipMagic,The magic value of the KIP file was not KIP1. +428,1004,,InvalidKipSegmentSize,The size of the compressed KIP segment was smaller than expected. +428,1005,,KipSegmentDecompressionFailed,An error occurred while decompressing a KIP segment. diff --git a/src/LibHac/Common/ResultLibHac.cs b/src/LibHac/Common/ResultLibHac.cs index 5114db1c..ddb27727 100644 --- a/src/LibHac/Common/ResultLibHac.cs +++ b/src/LibHac/Common/ResultLibHac.cs @@ -9,6 +9,8 @@ // code generation portion of the build. //----------------------------------------------------------------------------- +using System.Runtime.CompilerServices; + namespace LibHac.Common { public static class ResultLibHac @@ -23,5 +25,18 @@ namespace LibHac.Common public static Result.Base ArgumentOutOfRange => new Result.Base(ModuleLibHac, 3); /// Error code: 2428-0004; Inner value: 0x9ac public static Result.Base BufferTooSmall => new Result.Base(ModuleLibHac, 4); + + /// Error code: 2428-1000; Range: 1000-1999; Inner value: 0x7d1ac + public static Result.Base InvalidData { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleLibHac, 1000, 1999); } + /// Error code: 2428-1001; Range: 1001-1019; Inner value: 0x7d3ac + public static Result.Base InvalidKip { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleLibHac, 1001, 1019); } + /// The size of the KIP file was smaller than expected.
Error code: 2428-1002; Inner value: 0x7d5ac
+ public static Result.Base InvalidKipFileSize => new Result.Base(ModuleLibHac, 1002); + /// The magic value of the KIP file was not KIP1.
Error code: 2428-1003; Inner value: 0x7d7ac
+ public static Result.Base InvalidKipMagic => new Result.Base(ModuleLibHac, 1003); + /// The size of the compressed KIP segment was smaller than expected.
Error code: 2428-1004; Inner value: 0x7d9ac
+ public static Result.Base InvalidKipSegmentSize => new Result.Base(ModuleLibHac, 1004); + /// An error occurred while decompressing a KIP segment.
Error code: 2428-1005; Inner value: 0x7dbac
+ public static Result.Base KipSegmentDecompressionFailed => new Result.Base(ModuleLibHac, 1005); } } diff --git a/src/LibHac/Kip.cs b/src/LibHac/Kip.cs index 839bc544..ceaf5f55 100644 --- a/src/LibHac/Kip.cs +++ b/src/LibHac/Kip.cs @@ -5,6 +5,7 @@ using LibHac.FsSystem; namespace LibHac { + [Obsolete("This class has been deprecated. LibHac.Loader.KipReader should be used instead.")] public class Kip { private const int HeaderSize = 0x100; diff --git a/src/LibHac/Loader/KipHeader.cs b/src/LibHac/Loader/KipHeader.cs new file mode 100644 index 00000000..580b6471 --- /dev/null +++ b/src/LibHac/Loader/KipHeader.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Loader +{ + [StructLayout(LayoutKind.Explicit, Size = 0x100)] + public struct KipHeader + { + public const uint Kip1Magic = 0x3150494B; // KIP1 + public const int NameSize = 12; + public const int SegmentCount = 6; + + [FieldOffset(0x00)] public uint Magic; + + [FieldOffset(0x04)] private byte _name; + + [FieldOffset(0x10)] public ulong ProgramId; + [FieldOffset(0x18)] public int Version; + + [FieldOffset(0x1C)] public byte Priority; + [FieldOffset(0x1D)] public byte IdealCoreId; + [FieldOffset(0x1F)] public Flag Flags; + + [FieldOffset(0x20)] public int TextMemoryOffset; + [FieldOffset(0x24)] public int TextSize; + [FieldOffset(0x28)] public int TextFileSize; + + [FieldOffset(0x2C)] public int AffinityMask; + + [FieldOffset(0x30)] public int RoMemoryOffset; + [FieldOffset(0x34)] public int RoSize; + [FieldOffset(0x38)] public int RoFileSize; + + [FieldOffset(0x3C)] public int StackSize; + + [FieldOffset(0x40)] public int DataMemoryOffset; + [FieldOffset(0x44)] public int DataSize; + [FieldOffset(0x48)] public int DataFileSize; + + [FieldOffset(0x50)] public int BssMemoryOffset; + [FieldOffset(0x54)] public int BssSize; + [FieldOffset(0x58)] public int BssFileSize; + + [FieldOffset(0x80)] private uint _capabilities; + + public Span Name => SpanHelpers.CreateSpan(ref _name, NameSize); + + public Span Segments => + SpanHelpers.CreateSpan(ref Unsafe.As(ref TextMemoryOffset), SegmentCount); + + public Span Capabilities => SpanHelpers.CreateSpan(ref _capabilities, 0x80 / sizeof(uint)); + + public bool IsValid => Magic == Kip1Magic; + + [Flags] + public enum Flag : byte + { + TextCompress = 1 << 0, + RoCompress = 1 << 1, + DataCompress = 1 << 2, + Is64BitInstruction = 1 << 3, + ProcessAddressSpace64Bit = 1 << 4, + UseSecureMemory = 1 << 5 + } + + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct SegmentHeader + { + public int MemoryOffset; + public int Size; + public int FileSize; + } + } +} diff --git a/src/LibHac/Loader/KipReader.cs b/src/LibHac/Loader/KipReader.cs new file mode 100644 index 00000000..649d7f00 --- /dev/null +++ b/src/LibHac/Loader/KipReader.cs @@ -0,0 +1,265 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.Loader +{ + public class KipReader + { + private IFile KipFile { get; set; } + + private KipHeader _header; + + public ReadOnlySpan Capabilities => _header.Capabilities; + public U8Span Name => new U8Span(_header.Name); + + public ulong ProgramId => _header.ProgramId; + public int Version => _header.Version; + public byte Priority => _header.Priority; + public byte IdealCoreId => _header.IdealCoreId; + + public bool IsTextCompressed => _header.Flags.HasFlag(KipHeader.Flag.TextCompress); + public bool IsRoCompressed => _header.Flags.HasFlag(KipHeader.Flag.RoCompress); + public bool IsDataCompressed => _header.Flags.HasFlag(KipHeader.Flag.DataCompress); + public bool Is64Bit => _header.Flags.HasFlag(KipHeader.Flag.Is64BitInstruction); + public bool Is64BitAddressSpace => _header.Flags.HasFlag(KipHeader.Flag.ProcessAddressSpace64Bit); + public bool UsesSecureMemory => _header.Flags.HasFlag(KipHeader.Flag.UseSecureMemory); + + public ReadOnlySpan Segments => _header.Segments; + + public int AffinityMask => _header.AffinityMask; + public int StackSize => _header.StackSize; + + public Result Initialize(IFile kipFile) + { + if (kipFile is null) + return ResultLibHac.NullArgument.Log(); + + Result rc = kipFile.Read(out long bytesRead, 0, SpanHelpers.AsByteSpan(ref _header), ReadOption.None); + if (rc.IsFailure()) return rc; + + if (bytesRead != Unsafe.SizeOf()) + return ResultLibHac.InvalidKipFileSize.Log(); + + if (!_header.IsValid) + return ResultLibHac.InvalidKipMagic.Log(); + + KipFile = kipFile; + return Result.Success; + } + + public Result GetSegmentSize(SegmentType segment, out int size) + { + switch (segment) + { + case SegmentType.Text: + case SegmentType.Ro: + case SegmentType.Data: + case SegmentType.Bss: + case SegmentType.Reserved1: + case SegmentType.Reserved2: + size = _header.Segments[(int)segment].Size; + return Result.Success; + default: + size = default; + return ResultLibHac.ArgumentOutOfRange.Log(); + } + } + + public int GetUncompressedSize() + { + int size = Unsafe.SizeOf(); + + for (int i = 0; i < Segments.Length; i++) + { + if (Segments[i].FileSize != 0) + { + size += Segments[i].Size; + } + } + + return size; + } + + public Result ReadSegment(SegmentType segment, Span buffer) + { + Result rc = GetSegmentSize(segment, out int segmentSize); + if (rc.IsFailure()) return rc; + + if (buffer.Length < segmentSize) + return ResultLibHac.BufferTooSmall.Log(); + + KipHeader.SegmentHeader segmentHeader = Segments[(int)segment]; + + // Return early for empty segments. + if (segmentHeader.Size == 0) + return Result.Success; + + // The segment is all zeros if it has no data. + if (segmentHeader.FileSize == 0) + { + buffer.Slice(0, segmentHeader.Size).Clear(); + return Result.Success; + } + + int offset = CalculateSegmentOffset((int)segment); + + // Read the segment data. + rc = KipFile.Read(out long bytesRead, offset, buffer.Slice(0, segmentHeader.FileSize), ReadOption.None); + if (rc.IsFailure()) return rc; + + if (bytesRead != segmentHeader.FileSize) + return ResultLibHac.InvalidKipFileSize.Log(); + + // Decompress if necessary. + bool isCompressed = segment switch + { + SegmentType.Text => IsTextCompressed, + SegmentType.Ro => IsRoCompressed, + SegmentType.Data => IsDataCompressed, + _ => false + }; + + if (isCompressed) + { + rc = DecompressBlz(buffer, segmentHeader.FileSize); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + public Result ReadUncompressedKip(Span buffer) + { + if (buffer.Length < GetUncompressedSize()) + return ResultLibHac.BufferTooSmall.Log(); + + Span segmentBuffer = buffer.Slice(Unsafe.SizeOf()); + + // Read each of the segments into the buffer. + for (int i = 0; i < Segments.Length; i++) + { + if (Segments[i].FileSize != 0) + { + Result rc = ReadSegment((SegmentType)i, segmentBuffer); + if (rc.IsFailure()) return rc; + + segmentBuffer = segmentBuffer.Slice(Segments[i].Size); + } + } + + // Copy the header to the buffer and update the sizes and flags. + ref KipHeader header = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); + header = _header; + + // Remove any compression flags. + const KipHeader.Flag compressFlagsMask = + ~(KipHeader.Flag.TextCompress | KipHeader.Flag.RoCompress | KipHeader.Flag.DataCompress); + + header.Flags &= compressFlagsMask; + + // Update each segment's uncompressed size. + foreach (ref KipHeader.SegmentHeader segment in header.Segments) + { + if (segment.FileSize != 0) + { + segment.FileSize = segment.Size; + } + } + + return Result.Success; + } + + private int CalculateSegmentOffset(int index) + { + Debug.Assert((uint)index <= (uint)SegmentType.Reserved2); + + int offset = Unsafe.SizeOf(); + ReadOnlySpan segments = Segments; + + for (int i = 0; i < index; i++) + { + offset += segments[i].FileSize; + } + + return offset; + } + + private static Result DecompressBlz(Span buffer, int compressedDataSize) + { + const int segmentFooterSize = 12; + + if (buffer.Length < segmentFooterSize) + return ResultLibHac.InvalidKipSegmentSize.Log(); + + // Parse the footer, endian agnostic. + Span footer = buffer.Slice(compressedDataSize - segmentFooterSize); + int totalCompSize = BinaryPrimitives.ReadInt32LittleEndian(footer); + int footerSize = BinaryPrimitives.ReadInt32LittleEndian(footer.Slice(4)); + int additionalSize = BinaryPrimitives.ReadInt32LittleEndian(footer.Slice(8)); + + if (buffer.Length < totalCompSize + additionalSize) + return ResultLibHac.BufferTooSmall.Log(); + + Span data = buffer; + + int inOffset = totalCompSize - footerSize; + int outOffset = totalCompSize + additionalSize; + + while (outOffset != 0) + { + byte control = data[--inOffset]; + + // Each bit in the control byte is a flag indicating compressed or not compressed. + for (int i = 0; i < 8; i++) + { + if ((control & 0x80) != 0) + { + if (inOffset < 2) + return ResultLibHac.KipSegmentDecompressionFailed.Log(); + + inOffset -= 2; + ushort segmentValue = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(inOffset)); + int segmentOffset = (segmentValue & 0x0FFF) + 3; + int segmentSize = Math.Min(((segmentValue >> 12) & 0xF) + 3, outOffset); + + outOffset -= segmentSize; + + for (int j = 0; j < segmentSize; j++) + { + data[outOffset + j] = data[outOffset + segmentOffset + j]; + } + } + else + { + if (inOffset < 1) + return ResultLibHac.KipSegmentDecompressionFailed.Log(); + + // Copy directly. + data[--outOffset] = data[--inOffset]; + } + control <<= 1; + + if (outOffset == 0) + return Result.Success; + } + } + + return Result.Success; + } + + public enum SegmentType + { + Text = 0, + Ro = 1, + Data = 2, + Bss = 3, + Reserved1 = 4, + Reserved2 = 5 + } + } +} diff --git a/src/LibHac/Lz4.cs b/src/LibHac/Lz4.cs index 05be43e0..0ac165aa 100644 --- a/src/LibHac/Lz4.cs +++ b/src/LibHac/Lz4.cs @@ -82,6 +82,7 @@ namespace LibHac int cmpPos = 0; int decPos = 0; + // ReSharper disable once VariableHidesOuterVariable int GetLength(int length, ReadOnlySpan cmp) { byte sum; diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs index 386aa032..e0c4f28a 100644 --- a/src/hactoolnet/CliParser.cs +++ b/src/hactoolnet/CliParser.cs @@ -38,6 +38,7 @@ namespace hactoolnet new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]), new CliOption("outfile", 1, (o, a) => o.OutFile = a[0]), new CliOption("plaintext", 1, (o, a) => o.PlaintextOut = a[0]), + new CliOption("uncompressed", 1, (o, a) => o.UncompressedOut = a[0]), new CliOption("nspout", 1, (o, a) => o.NspOut = a[0]), new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), @@ -221,6 +222,8 @@ namespace hactoolnet sb.AppendLine(" --romfsdir Specify RomFS directory path."); sb.AppendLine(" --listromfs List files in RomFS."); sb.AppendLine(" --basenca Set Base NCA to use with update partitions."); + sb.AppendLine("KIP1 options:"); + sb.AppendLine(" --uncompressed Specify file path for saving uncompressed KIP1."); sb.AppendLine("RomFS options:"); sb.AppendLine(" --romfsdir Specify RomFS directory path."); sb.AppendLine(" --listromfs List files in RomFS."); diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index 152a3c80..4a633aab 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -29,6 +29,7 @@ namespace hactoolnet public string OutDir; public string OutFile; public string PlaintextOut; + public string UncompressedOut; public string SdSeed; public string NspOut; public string SdPath; diff --git a/src/hactoolnet/ProcessKip.cs b/src/hactoolnet/ProcessKip.cs index 1cbfacbc..1b36225c 100644 --- a/src/hactoolnet/ProcessKip.cs +++ b/src/hactoolnet/ProcessKip.cs @@ -1,6 +1,8 @@ using System.IO; using LibHac; +using LibHac.Fs; using LibHac.FsSystem; +using LibHac.Loader; namespace hactoolnet { @@ -8,10 +10,19 @@ namespace hactoolnet { public static void ProcessKip1(Context ctx) { - using (var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read)) + using (var file = new LocalFile(ctx.Options.InFile, OpenMode.Read)) { - var kip = new Kip(file); - kip.OpenRawFile(); + var kip = new KipReader(); + kip.Initialize(file).ThrowIfFailure(); + + if (!string.IsNullOrWhiteSpace(ctx.Options.UncompressedOut)) + { + var uncompressed = new byte[kip.GetUncompressedSize()]; + + kip.ReadUncompressedKip(uncompressed).ThrowIfFailure(); + + File.WriteAllBytes(ctx.Options.UncompressedOut, uncompressed); + } } }