diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 67a87da8..23e8384d 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -214,38 +214,34 @@ namespace hactoolnet { using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read)) { - var save = new Savefile(file); + var save = new Savefile(file, ctx.Logger); var layout = save.Header.Layout; - save.FileRemap.Position = layout.DuplexL1OffsetB; - using (var outFile = new FileStream("0_DuplexL1A", FileMode.Create, FileAccess.Write)) - { - save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); - } + File.WriteAllBytes("d0_JournalTable", save.JournalTable); + File.WriteAllBytes("d1_JournalBitmapUpdatedPhysical", save.JournalBitmapUpdatedPhysical); + File.WriteAllBytes("d2_JournalBitmapUpdatedVirtual", save.JournalBitmapUpdatedVirtual); + File.WriteAllBytes("d3_JournalBitmapUnassigned", save.JournalBitmapUnassigned); + File.WriteAllBytes("d4_Layer1Hash", save.JournalLayer1Hash); + File.WriteAllBytes("d5_Layer2Hash", save.JournalLayer2Hash); + File.WriteAllBytes("d6_Layer3Hash", save.JournalLayer3Hash); + File.WriteAllBytes("d7_Stuff", save.JournalStuff); - save.FileRemap.Position = layout.DuplexL1OffsetB; - using (var outFile = new FileStream("1_DuplexL1B", FileMode.Create, FileAccess.Write)) - { - save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); - } - - save.FileRemap.Position = layout.DuplexDataOffsetA; - using (var outFile = new FileStream("2_DuplexDataA", FileMode.Create, FileAccess.Write)) - { - save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); - } - - save.FileRemap.Position = layout.DuplexDataOffsetB; - using (var outFile = new FileStream("3_DuplexDataB", FileMode.Create, FileAccess.Write)) - { - save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); - } + File.WriteAllBytes("0_DuplexL1A", save.DuplexL1A); + File.WriteAllBytes("1_DuplexL1B", save.DuplexL1B); + File.WriteAllBytes("2_DuplexDataA", save.DuplexDataA); + File.WriteAllBytes("3_DuplexDataB", save.DuplexDataB); save.FileRemap.Position = layout.JournalDataOffset; using (var outFile = new FileStream("4_JournalData", FileMode.Create, FileAccess.Write)) { save.FileRemap.CopyStream(outFile, layout.JournalDataSizeB + layout.SizeReservedArea); } + + save.JournalStream.Position = 0; + using (var outFile = new FileStream("j0_Data", FileMode.Create, FileAccess.Write)) + { + save.JournalStream.CopyStream(outFile, save.JournalStream.Length); + } } } diff --git a/libhac/BitReader.cs b/libhac/BitReader.cs new file mode 100644 index 00000000..9f0a12a2 --- /dev/null +++ b/libhac/BitReader.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics; + +namespace libhac +{ + public class BitReader + { + public byte[] Buffer { get; private set; } + public int LengthBits { get; private set; } + public int Position { get; set; } + public int Remaining => LengthBits - Position; + + public BitReader(byte[] buffer) => SetBuffer(buffer); + + public void SetBuffer(byte[] buffer) + { + Buffer = buffer; + LengthBits = Buffer?.Length * 8 ?? 0; + Position = 0; + } + + public int ReadInt(int bitCount) + { + int value = PeekInt(bitCount); + Position += bitCount; + return value; + } + + //public int ReadSignedInt(int bitCount) + //{ + // int value = PeekInt(bitCount); + // Position += bitCount; + // return Bit.SignExtend32(value, bitCount); + //} + + public bool ReadBool() => ReadInt(1) == 1; + + public int ReadOffsetBinary(int bitCount, OffsetBias bias) + { + int offset = (1 << (bitCount - 1)) - (int)bias; + int value = PeekInt(bitCount) - offset; + Position += bitCount; + return value; + } + + //public void AlignPosition(int multiple) + //{ + // Position = Helpers.GetNextMultiple(Position, multiple); + //} + + public int PeekInt(int bitCount) + { + Debug.Assert(bitCount >= 0 && bitCount <= 32); + + if (bitCount > Remaining) + { + if (Position >= LengthBits) return 0; + + int extraBits = bitCount - Remaining; + return PeekIntFallback(Remaining) << extraBits; + } + + int byteIndex = Position / 8; + int bitIndex = Position % 8; + + if (bitCount <= 9 && Remaining >= 16) + { + int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1]; + value &= 0xFFFF >> bitIndex; + value >>= 16 - bitCount - bitIndex; + return value; + } + + if (bitCount <= 17 && Remaining >= 24) + { + int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2]; + value &= 0xFFFFFF >> bitIndex; + value >>= 24 - bitCount - bitIndex; + return value; + } + + if (bitCount <= 25 && Remaining >= 32) + { + int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3]; + value &= (int)(0xFFFFFFFF >> bitIndex); + value >>= 32 - bitCount - bitIndex; + return value; + } + return PeekIntFallback(bitCount); + } + + private int PeekIntFallback(int bitCount) + { + int value = 0; + int byteIndex = Position / 8; + int bitIndex = Position % 8; + + while (bitCount > 0) + { + if (bitIndex >= 8) + { + bitIndex = 0; + byteIndex++; + } + + int bitsToRead = Math.Min(bitCount, 8 - bitIndex); + int mask = 0xFF >> bitIndex; + int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead); + + value = (value << bitsToRead) | currentByte; + bitIndex += bitsToRead; + bitCount -= bitsToRead; + } + return value; + } + + /// + /// Specifies the bias of an offset binary value. A positive bias can represent one more + /// positive value than negative value, and a negative bias can represent one more + /// negative value than positive value. + /// + /// Example: + /// A 4-bit offset binary value with a positive bias can store + /// the values 8 through -7 inclusive. + /// A 4-bit offset binary value with a positive bias can store + /// the values 7 through -8 inclusive. + public enum OffsetBias + { + Positive = 1, + Negative = 0 + } + } +} diff --git a/libhac/Savefile/Header.cs b/libhac/Savefile/Header.cs index 6b79c096..703c4637 100644 --- a/libhac/Savefile/Header.cs +++ b/libhac/Savefile/Header.cs @@ -7,6 +7,7 @@ namespace libhac.Savefile { public byte[] Cmac { get; set; } public FsLayout Layout { get; set; } + public JournalHeader Journal{ get; set; } public RemapHeader FileRemap { get; set; } public RemapHeader MetaRemap { get; set; } @@ -27,6 +28,9 @@ namespace libhac.Savefile reader.BaseStream.Position = 0x100; Layout = new FsLayout(reader); + reader.BaseStream.Position = 0x408; + Journal = new JournalHeader(reader); + reader.BaseStream.Position = 0x650; FileRemap = new RemapHeader(reader); reader.BaseStream.Position = 0x690; @@ -87,8 +91,8 @@ namespace libhac.Savefile public long MasterHashOffset0 { get; set; } public long MasterHashOffset1 { get; set; } public long MasterHashSize { get; set; } - public long OffsetJournalTable { get; set; } - public long SizeJournalTable { get; set; } + public long JournalTableOffset { get; set; } + public long JournalTableSize { get; set; } public long JournalBitmapUpdatedPhysicalOffset { get; set; } public long JournalBitmapUpdatedPhysicalSize { get; set; } public long JournalBitmapUpdatedVirtualOffset { get; set; } @@ -132,8 +136,8 @@ namespace libhac.Savefile MasterHashOffset0 = reader.ReadInt64(); MasterHashOffset1 = reader.ReadInt64(); MasterHashSize = reader.ReadInt64(); - OffsetJournalTable = reader.ReadInt64(); - SizeJournalTable = reader.ReadInt64(); + JournalTableOffset = reader.ReadInt64(); + JournalTableSize = reader.ReadInt64(); JournalBitmapUpdatedPhysicalOffset = reader.ReadInt64(); JournalBitmapUpdatedPhysicalSize = reader.ReadInt64(); JournalBitmapUpdatedVirtualOffset = reader.ReadInt64(); @@ -170,6 +174,32 @@ namespace libhac.Savefile } } + public class JournalHeader + { + public string Magic { get; } + public uint MagicNum { get; } + public long Field8 { get; } + public long Field10 { get; } + public long BlockSize { get; } + public int Field20 { get; } + public int MappingEntryCount { get; } + public int Field28 { get; } + public int Field2C { get; } + + public JournalHeader(BinaryReader reader) + { + Magic = reader.ReadAscii(4); + MagicNum = reader.ReadUInt32(); + Field8 = reader.ReadInt64(); + Field10 = reader.ReadInt64(); + BlockSize = reader.ReadInt64(); + Field20 = reader.ReadInt32(); + MappingEntryCount = reader.ReadInt32(); + Field28 = reader.ReadInt32(); + Field2C = reader.ReadInt32(); + } + } + public class MapEntry { public long VirtualOffset { get; } diff --git a/libhac/Savefile/Journal.cs b/libhac/Savefile/Journal.cs new file mode 100644 index 00000000..a68359e7 --- /dev/null +++ b/libhac/Savefile/Journal.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; + +namespace libhac.Savefile +{ + public class JournalStream : Stream + { + private long _position; + private Stream BaseStream { get; } + public MappingEntry[] Map { get; } + public int BlockSize { get; } + private MappingEntry CurrentMapEntry { get; set; } + + public JournalStream(Stream baseStream, MappingEntry[] map, int blockSize) + { + BaseStream = baseStream; + Map = map; + BlockSize = blockSize; + Length = map.Length * BlockSize; + } + + public override int Read(byte[] buffer, int offset, int count) + { + long remaining = Length - Position; + if (remaining <= 0) return 0; + if (remaining < count) count = (int)remaining; + + var toOutput = count; + int outPos = offset; + + while (toOutput > 0) + { + var remainInEntry = BlockSize - Position % BlockSize; + int toRead = (int)Math.Min(toOutput, remainInEntry); + BaseStream.Read(buffer, outPos, toRead); + + outPos += toRead; + toOutput -= toRead; + Position += toRead; + } + + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = Length - offset; + break; + } + + return Position; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length { get; } + public override long Position + { + get => _position; + set + { + _position = value; + if (value >= Length) return; + var currentBlock = value / BlockSize; + var blockPos = value % BlockSize; + CurrentMapEntry = Map[currentBlock]; + BaseStream.Position = CurrentMapEntry.PhysicalIndex * BlockSize + blockPos; + } + } + + public static MappingEntry[] ReadMappingEntries(byte[] mapTable, byte[] bitmapUpdatedPhysical, + byte[] bitmapUpdatedVirtual, byte[] bitmapUnassigned, int count) + { + var physicalBits = new BitReader(bitmapUpdatedPhysical); + var virtualBits = new BitReader(bitmapUpdatedVirtual); + var unassignedBits = new BitReader(bitmapUnassigned); + var tableReader = new BinaryReader(new MemoryStream(mapTable)); + var map = new MappingEntry[count]; + + for (int i = 0; i < count; i++) + { + var entry = new MappingEntry + { + VirtualIndex = i, + PhysicalIndex = tableReader.ReadInt32() & 0x7FFFFFFF, + //UpdatedPhysical = physicalBits.ReadBool(), + //UpdatedVirtual = virtualBits.ReadBool(), + //Unassigned = unassignedBits.ReadBool() + }; + + map[i] = entry; + tableReader.BaseStream.Position += 4; + } + + return map; + } + } + + public class MappingEntry + { + public int PhysicalIndex { get; set; } + public int VirtualIndex { get; set; } + public bool UpdatedPhysical { get; set; } + public bool UpdatedVirtual { get; set; } + public bool Unassigned { get; set; } + } +} diff --git a/libhac/Savefile/Savefile.cs b/libhac/Savefile/Savefile.cs index 5fb656a6..59da839a 100644 --- a/libhac/Savefile/Savefile.cs +++ b/libhac/Savefile/Savefile.cs @@ -7,15 +7,85 @@ namespace libhac.Savefile { public Header Header { get; } public RemapStream FileRemap { get; } + public RemapStream MetaRemap { get; } + private Stream FileStream { get; } + public JournalStream JournalStream { get; } - public Savefile(Stream file) + public byte[] DuplexL1A { get; } + public byte[] DuplexL1B { get; } + public byte[] DuplexDataA { get; } + public byte[] DuplexDataB { get; } + + public byte[] JournalTable { get; } + public byte[] JournalBitmapUpdatedPhysical { get; } + public byte[] JournalBitmapUpdatedVirtual { get; } + public byte[] JournalBitmapUnassigned { get; } + public byte[] JournalLayer1Hash { get; } + public byte[] JournalLayer2Hash { get; } + public byte[] JournalLayer3Hash { get; } + public byte[] JournalStuff { get; } + + public Savefile(Stream file, IProgressReport logger = null) { + FileStream = file; using (var reader = new BinaryReader(file, Encoding.Default, true)) { - Header = new Header(reader); + Header = new Header(reader, logger); + var layout = Header.Layout; FileRemap = new RemapStream( - new SubStream(file, Header.Layout.FileMapDataOffset, Header.Layout.FileMapDataSize), + new SubStream(file, layout.FileMapDataOffset, layout.FileMapDataSize), Header.FileMapEntries, Header.FileRemap.MapSegmentCount); + + DuplexL1A = new byte[layout.DuplexL1Size]; + DuplexL1B = new byte[layout.DuplexL1Size]; + DuplexDataA = new byte[layout.DuplexDataSize]; + DuplexDataB = new byte[layout.DuplexDataSize]; + + FileRemap.Position = layout.DuplexL1OffsetA; + FileRemap.Read(DuplexL1A, 0, DuplexL1A.Length); + FileRemap.Position = layout.DuplexL1OffsetB; + FileRemap.Read(DuplexL1B, 0, DuplexL1B.Length); + FileRemap.Position = layout.DuplexDataOffsetA; + FileRemap.Read(DuplexDataA, 0, DuplexDataA.Length); + FileRemap.Position = layout.DuplexDataOffsetB; + FileRemap.Read(DuplexDataB, 0, DuplexDataB.Length); + + var duplexData = new SubStream(FileRemap, layout.DuplexDataOffsetB, layout.DuplexDataSize); + MetaRemap = new RemapStream(duplexData, Header.MetaMapEntries, Header.MetaRemap.MapSegmentCount); + + JournalTable = new byte[layout.JournalTableSize]; + JournalBitmapUpdatedPhysical = new byte[layout.JournalBitmapUpdatedPhysicalSize]; + JournalBitmapUpdatedVirtual = new byte[layout.JournalBitmapUpdatedVirtualSize]; + JournalBitmapUnassigned = new byte[layout.JournalBitmapUnassignedSize]; + JournalLayer1Hash = new byte[layout.Layer1HashSize]; + JournalLayer2Hash = new byte[layout.Layer2HashSize]; + JournalLayer3Hash = new byte[layout.Layer3HashSize]; + JournalStuff = new byte[layout.Field150]; + + MetaRemap.Position = layout.JournalTableOffset; + MetaRemap.Read(JournalTable, 0, JournalTable.Length); + MetaRemap.Position = layout.JournalBitmapUpdatedPhysicalOffset; + MetaRemap.Read(JournalBitmapUpdatedPhysical, 0, JournalBitmapUpdatedPhysical.Length); + MetaRemap.Position = layout.JournalBitmapUpdatedVirtualOffset; + MetaRemap.Read(JournalBitmapUpdatedVirtual, 0, JournalBitmapUpdatedVirtual.Length); + MetaRemap.Position = layout.JournalBitmapUnassignedOffset; + MetaRemap.Read(JournalBitmapUnassigned, 0, JournalBitmapUnassigned.Length); + MetaRemap.Position = layout.Layer1HashOffset; + MetaRemap.Read(JournalLayer1Hash, 0, JournalLayer1Hash.Length); + MetaRemap.Position = layout.Layer2HashOffset; + MetaRemap.Read(JournalLayer2Hash, 0, JournalLayer2Hash.Length); + MetaRemap.Position = layout.Layer3HashOffset; + MetaRemap.Read(JournalLayer3Hash, 0, JournalLayer3Hash.Length); + MetaRemap.Position = layout.Field148; + MetaRemap.Read(JournalStuff, 0, JournalStuff.Length); + + var journalMap = JournalStream.ReadMappingEntries(JournalTable, JournalBitmapUpdatedPhysical, + JournalBitmapUpdatedVirtual, JournalBitmapUnassigned, Header.Journal.MappingEntryCount); + + var journalData = new SubStream(FileRemap, layout.JournalDataOffset, + layout.JournalDataSizeB + layout.SizeReservedArea); + JournalStream = new JournalStream(journalData, journalMap, (int) Header.Journal.BlockSize) + ; } } }