From ae649182ce1039b1d245a91ce32df92794beb89c Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 13 Oct 2018 15:12:10 -0500 Subject: [PATCH 01/13] Rename "Savefile" namespace to "Save" --- LibHac/{Savefile => Save}/AllocationTable.cs | 2 +- LibHac/{Savefile => Save}/AllocationTableIterator.cs | 2 +- LibHac/{Savefile => Save}/AllocationTableStream.cs | 2 +- LibHac/{Savefile => Save}/DuplexBitmap.cs | 2 +- LibHac/{Savefile => Save}/DuplexFs.cs | 2 +- LibHac/{Savefile => Save}/FsEntry.cs | 2 +- LibHac/{Savefile => Save}/Header.cs | 2 +- LibHac/{Savefile => Save}/Journal.cs | 2 +- LibHac/{Savefile => Save}/LayeredDuplexFs.cs | 2 +- LibHac/{Savefile => Save}/RemapStream.cs | 2 +- LibHac/{Savefile => Save}/Savefile.cs | 2 +- LibHac/SwitchFs.cs | 7 ++++--- NandReader/Program.cs | 2 +- NandReaderGui/ViewModel/NandViewModel.cs | 2 +- hactoolnet/ProcessSave.cs | 2 +- hactoolnet/ProcessSwitchFs.cs | 2 +- 16 files changed, 19 insertions(+), 18 deletions(-) rename LibHac/{Savefile => Save}/AllocationTable.cs (97%) rename LibHac/{Savefile => Save}/AllocationTableIterator.cs (99%) rename LibHac/{Savefile => Save}/AllocationTableStream.cs (99%) rename LibHac/{Savefile => Save}/DuplexBitmap.cs (97%) rename LibHac/{Savefile => Save}/DuplexFs.cs (99%) rename LibHac/{Savefile => Save}/FsEntry.cs (99%) rename LibHac/{Savefile => Save}/Header.cs (99%) rename LibHac/{Savefile => Save}/Journal.cs (99%) rename LibHac/{Savefile => Save}/LayeredDuplexFs.cs (98%) rename LibHac/{Savefile => Save}/RemapStream.cs (99%) rename LibHac/{Savefile => Save}/Savefile.cs (99%) diff --git a/LibHac/Savefile/AllocationTable.cs b/LibHac/Save/AllocationTable.cs similarity index 97% rename from LibHac/Savefile/AllocationTable.cs rename to LibHac/Save/AllocationTable.cs index 910c4513..4bd42f21 100644 --- a/LibHac/Savefile/AllocationTable.cs +++ b/LibHac/Save/AllocationTable.cs @@ -1,6 +1,6 @@ using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class AllocationTable { diff --git a/LibHac/Savefile/AllocationTableIterator.cs b/LibHac/Save/AllocationTableIterator.cs similarity index 99% rename from LibHac/Savefile/AllocationTableIterator.cs rename to LibHac/Save/AllocationTableIterator.cs index 8bd477aa..5065f5d8 100644 --- a/LibHac/Savefile/AllocationTableIterator.cs +++ b/LibHac/Save/AllocationTableIterator.cs @@ -1,6 +1,6 @@ using System; -namespace LibHac.Savefile +namespace LibHac.Save { public class AllocationTableIterator { diff --git a/LibHac/Savefile/AllocationTableStream.cs b/LibHac/Save/AllocationTableStream.cs similarity index 99% rename from LibHac/Savefile/AllocationTableStream.cs rename to LibHac/Save/AllocationTableStream.cs index 5e7ac86f..c8bcdf8c 100644 --- a/LibHac/Savefile/AllocationTableStream.cs +++ b/LibHac/Save/AllocationTableStream.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class AllocationTableStream : Stream { diff --git a/LibHac/Savefile/DuplexBitmap.cs b/LibHac/Save/DuplexBitmap.cs similarity index 97% rename from LibHac/Savefile/DuplexBitmap.cs rename to LibHac/Save/DuplexBitmap.cs index 03fc2f54..cb691c7b 100644 --- a/LibHac/Savefile/DuplexBitmap.cs +++ b/LibHac/Save/DuplexBitmap.cs @@ -2,7 +2,7 @@ using System.Collections; using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class DuplexBitmap { diff --git a/LibHac/Savefile/DuplexFs.cs b/LibHac/Save/DuplexFs.cs similarity index 99% rename from LibHac/Savefile/DuplexFs.cs rename to LibHac/Save/DuplexFs.cs index eae08cd1..86282822 100644 --- a/LibHac/Savefile/DuplexFs.cs +++ b/LibHac/Save/DuplexFs.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class DuplexFs : Stream { diff --git a/LibHac/Savefile/FsEntry.cs b/LibHac/Save/FsEntry.cs similarity index 99% rename from LibHac/Savefile/FsEntry.cs rename to LibHac/Save/FsEntry.cs index ab9ab87e..95147123 100644 --- a/LibHac/Savefile/FsEntry.cs +++ b/LibHac/Save/FsEntry.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.IO; using System.Text; -namespace LibHac.Savefile +namespace LibHac.Save { [DebuggerDisplay("{" + nameof(FullPath) + "}")] public abstract class FsEntry diff --git a/LibHac/Savefile/Header.cs b/LibHac/Save/Header.cs similarity index 99% rename from LibHac/Savefile/Header.cs rename to LibHac/Save/Header.cs index 60b8fe1c..e3f9fda6 100644 --- a/LibHac/Savefile/Header.cs +++ b/LibHac/Save/Header.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class Header { diff --git a/LibHac/Savefile/Journal.cs b/LibHac/Save/Journal.cs similarity index 99% rename from LibHac/Savefile/Journal.cs rename to LibHac/Save/Journal.cs index ad511206..23c8dea3 100644 --- a/LibHac/Savefile/Journal.cs +++ b/LibHac/Save/Journal.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class JournalStream : Stream { diff --git a/LibHac/Savefile/LayeredDuplexFs.cs b/LibHac/Save/LayeredDuplexFs.cs similarity index 98% rename from LibHac/Savefile/LayeredDuplexFs.cs rename to LibHac/Save/LayeredDuplexFs.cs index 124082f1..7042eb0f 100644 --- a/LibHac/Savefile/LayeredDuplexFs.cs +++ b/LibHac/Save/LayeredDuplexFs.cs @@ -1,6 +1,6 @@ using System.IO; -namespace LibHac.Savefile +namespace LibHac.Save { public class LayeredDuplexFs : Stream { diff --git a/LibHac/Savefile/RemapStream.cs b/LibHac/Save/RemapStream.cs similarity index 99% rename from LibHac/Savefile/RemapStream.cs rename to LibHac/Save/RemapStream.cs index f0f5e735..e1adab68 100644 --- a/LibHac/Savefile/RemapStream.cs +++ b/LibHac/Save/RemapStream.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -namespace LibHac.Savefile +namespace LibHac.Save { public class RemapStream : Stream { diff --git a/LibHac/Savefile/Savefile.cs b/LibHac/Save/Savefile.cs similarity index 99% rename from LibHac/Savefile/Savefile.cs rename to LibHac/Save/Savefile.cs index cee44595..f5347391 100644 --- a/LibHac/Savefile/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -4,7 +4,7 @@ using System.Security.Cryptography; using System.Text; using LibHac.Streams; -namespace LibHac.Savefile +namespace LibHac.Save { public class Savefile { diff --git a/LibHac/SwitchFs.cs b/LibHac/SwitchFs.cs index 967aa14e..196c2379 100644 --- a/LibHac/SwitchFs.cs +++ b/LibHac/SwitchFs.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using LibHac.Save; using LibHac.Streams; namespace LibHac @@ -16,7 +17,7 @@ namespace LibHac public string SaveDir { get; } public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - public Dictionary Saves { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Saves { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Titles { get; } = new Dictionary(); public Dictionary Applications { get; } = new Dictionary(); @@ -117,7 +118,7 @@ namespace LibHac foreach (string file in files) { - Savefile.Savefile save = null; + Savefile save = null; string saveName = Path.GetFileNameWithoutExtension(file); try @@ -126,7 +127,7 @@ namespace LibHac string sdPath = "/" + Util.GetRelativePath(file, SaveDir).Replace('\\', '/'); var nax0 = new Nax0(Keyset, stream, sdPath, false); - save = new Savefile.Savefile(Keyset, nax0.Stream, IntegrityCheckLevel.None); + save = new Savefile(Keyset, nax0.Stream, IntegrityCheckLevel.None); } catch (Exception ex) { diff --git a/NandReader/Program.cs b/NandReader/Program.cs index 4718535d..8fd0fc9e 100644 --- a/NandReader/Program.cs +++ b/NandReader/Program.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.IO; using LibHac; using LibHac.Nand; -using LibHac.Savefile; +using LibHac.Save; namespace NandReader { diff --git a/NandReaderGui/ViewModel/NandViewModel.cs b/NandReaderGui/ViewModel/NandViewModel.cs index feddd0ad..1f99cf2d 100644 --- a/NandReaderGui/ViewModel/NandViewModel.cs +++ b/NandReaderGui/ViewModel/NandViewModel.cs @@ -7,7 +7,7 @@ using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using LibHac; using LibHac.Nand; -using LibHac.Savefile; +using LibHac.Save; using LibHac.Streams; namespace NandReaderGui.ViewModel diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index 060f3c3c..10d103b5 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using System.Text; using LibHac; -using LibHac.Savefile; +using LibHac.Save; using static hactoolnet.Print; namespace hactoolnet diff --git a/hactoolnet/ProcessSwitchFs.cs b/hactoolnet/ProcessSwitchFs.cs index fe2e89cd..a346bee5 100644 --- a/hactoolnet/ProcessSwitchFs.cs +++ b/hactoolnet/ProcessSwitchFs.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Text; using LibHac; -using LibHac.Savefile; +using LibHac.Save; namespace hactoolnet { From 6935983b710baf8b6a2e5d3ea82b309317b56204 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 14 Oct 2018 18:00:46 -0500 Subject: [PATCH 02/13] Add Seek to AllocationTableStream --- LibHac/Save/AllocationTableStream.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/LibHac/Save/AllocationTableStream.cs b/LibHac/Save/AllocationTableStream.cs index c8bcdf8c..49785789 100644 --- a/LibHac/Save/AllocationTableStream.cs +++ b/LibHac/Save/AllocationTableStream.cs @@ -53,7 +53,20 @@ namespace LibHac.Save public override long Seek(long offset, SeekOrigin origin) { - throw new NotImplementedException(); + 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) From 75cb28364feb510fd3f651efa2676d2d503c3080 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 14 Oct 2018 19:23:18 -0500 Subject: [PATCH 03/13] Create SaveFs class --- LibHac/Save/SaveFs.cs | 147 ++++++++++++++++++++++++++++++++++++++++ LibHac/Save/Savefile.cs | 129 +++-------------------------------- 2 files changed, 158 insertions(+), 118 deletions(-) create mode 100644 LibHac/Save/SaveFs.cs diff --git a/LibHac/Save/SaveFs.cs b/LibHac/Save/SaveFs.cs new file mode 100644 index 00000000..533db211 --- /dev/null +++ b/LibHac/Save/SaveFs.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.IO; +using LibHac.Streams; + +namespace LibHac.Save +{ + public class SaveFs + { + private AllocationTable AllocationTable { get; } + private SharedStreamSource StreamSource { get; } + private SaveHeader Header { get; } + + public DirectoryEntry RootDirectory { get; private set; } + public FileEntry[] Files { get; private set; } + public DirectoryEntry[] Directories { get; private set; } + public Dictionary FileDictionary { get; } + + public SaveFs(Stream storage, Stream allocationTable, SaveHeader header) + { + StreamSource = new SharedStreamSource(storage); + AllocationTable = new AllocationTable(allocationTable); + Header = header; + + ReadFileInfo(); + var dictionary = new Dictionary(); + foreach (FileEntry entry in Files) + { + dictionary[entry.FullPath] = entry; + } + + FileDictionary = dictionary; + } + + public Stream OpenFile(string filename) + { + if (!FileDictionary.TryGetValue(filename, out FileEntry file)) + { + throw new FileNotFoundException(); + } + + return OpenFile(file); + } + + public Stream OpenFile(FileEntry file) + { + if (file.BlockIndex < 0) + { + return Stream.Null; + } + + return OpenFatBlock(file.BlockIndex, file.FileSize); + } + + public bool FileExists(string filename) => FileDictionary.ContainsKey(filename); + + public Stream OpenRawSaveFs() => StreamSource.CreateStream(); + + private void ReadFileInfo() + { + // todo: Query the FAT for the file size when none is given + AllocationTableStream dirTableStream = OpenFatBlock(Header.DirectoryTableBlock, 1000000); + AllocationTableStream fileTableStream = OpenFatBlock(Header.FileTableBlock, 1000000); + + DirectoryEntry[] dirEntries = ReadDirEntries(dirTableStream); + FileEntry[] fileEntries = ReadFileEntries(fileTableStream); + + foreach (DirectoryEntry dir in dirEntries) + { + if (dir.NextSiblingIndex != 0) dir.NextSibling = dirEntries[dir.NextSiblingIndex]; + if (dir.FirstChildIndex != 0) dir.FirstChild = dirEntries[dir.FirstChildIndex]; + if (dir.FirstFileIndex != 0) dir.FirstFile = fileEntries[dir.FirstFileIndex]; + if (dir.NextInChainIndex != 0) dir.NextInChain = dirEntries[dir.NextInChainIndex]; + if (dir.ParentDirIndex != 0 && dir.ParentDirIndex < dirEntries.Length) + dir.ParentDir = dirEntries[dir.ParentDirIndex]; + } + + foreach (FileEntry file in fileEntries) + { + if (file.NextSiblingIndex != 0) file.NextSibling = fileEntries[file.NextSiblingIndex]; + if (file.NextInChainIndex != 0) file.NextInChain = fileEntries[file.NextInChainIndex]; + if (file.ParentDirIndex != 0 && file.ParentDirIndex < dirEntries.Length) + file.ParentDir = dirEntries[file.ParentDirIndex]; + } + + RootDirectory = dirEntries[2]; + + FileEntry fileChain = fileEntries[1].NextInChain; + var files = new List(); + while (fileChain != null) + { + files.Add(fileChain); + fileChain = fileChain.NextInChain; + } + + DirectoryEntry dirChain = dirEntries[1].NextInChain; + var dirs = new List(); + while (dirChain != null) + { + dirs.Add(dirChain); + dirChain = dirChain.NextInChain; + } + + Files = files.ToArray(); + Directories = dirs.ToArray(); + + FsEntry.ResolveFilenames(Files); + FsEntry.ResolveFilenames(Directories); + } + + private FileEntry[] ReadFileEntries(Stream stream) + { + var reader = new BinaryReader(stream); + int count = reader.ReadInt32(); + + reader.BaseStream.Position -= 4; + + var entries = new FileEntry[count]; + for (int i = 0; i < count; i++) + { + entries[i] = new FileEntry(reader); + } + + return entries; + } + + private DirectoryEntry[] ReadDirEntries(Stream stream) + { + var reader = new BinaryReader(stream); + int count = reader.ReadInt32(); + + reader.BaseStream.Position -= 4; + + var entries = new DirectoryEntry[count]; + for (int i = 0; i < count; i++) + { + entries[i] = new DirectoryEntry(reader); + } + + return entries; + } + + private AllocationTableStream OpenFatBlock(int blockIndex, long size) + { + return new AllocationTableStream(StreamSource.CreateStream(), AllocationTable, (int)Header.BlockSize, blockIndex, size); + } + } +} diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index f5347391..af3bd616 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.IO; +using System.IO; using System.Security.Cryptography; using System.Text; using LibHac.Streams; @@ -18,7 +17,7 @@ namespace LibHac.Save public SharedStreamSource JournalStreamSource { get; } private HierarchicalIntegrityVerificationStream IvfcStream { get; } public SharedStreamSource IvfcStreamSource { get; } - private AllocationTable AllocationTable { get; } + public SaveFs SaveFs { get; } public Stream DuplexL1A { get; } public Stream DuplexL1B { get; } @@ -36,10 +35,9 @@ namespace LibHac.Save public Stream JournalLayer3Hash { get; } public Stream JournalFat { get; } - public DirectoryEntry RootDirectory { get; private set; } - public FileEntry[] Files { get; private set; } - public DirectoryEntry[] Directories { get; private set; } - private Dictionary FileDict { get; } + public DirectoryEntry RootDirectory => SaveFs.RootDirectory; + public FileEntry[] Files => SaveFs.Files; + public DirectoryEntry[] Directories => SaveFs.Directories; public Savefile(Keyset keyset, Stream file, IntegrityCheckLevel integrityCheckLevel) { @@ -97,7 +95,6 @@ namespace LibHac.Save JournalLayer2Hash = MetaRemapSource.CreateStream(layout.IvfcL2Offset, layout.IvfcL2Size); JournalLayer3Hash = MetaRemapSource.CreateStream(layout.IvfcL3Offset, layout.IvfcL3Size); JournalFat = MetaRemapSource.CreateStream(layout.FatOffset, layout.FatSize); - AllocationTable = new AllocationTable(JournalFat); MappingEntry[] journalMap = JournalStream.ReadMappingEntries(JournalTable, Header.Journal.MainDataBlockCount); @@ -107,16 +104,10 @@ namespace LibHac.Save JournalStreamSource = new SharedStreamSource(JournalStream); IvfcStream = InitIvfcStream(integrityCheckLevel); + + SaveFs = new SaveFs(IvfcStream, MetaRemapSource.CreateStream(layout.FatOffset, layout.FatSize), Header.Save); + IvfcStreamSource = new SharedStreamSource(IvfcStream); - - ReadFileInfo(); - var dictionary = new Dictionary(); - foreach (FileEntry entry in Files) - { - dictionary[entry.FullPath] = entry; - } - - FileDict = dictionary; } } @@ -156,114 +147,16 @@ namespace LibHac.Save public Stream OpenFile(string filename) { - if (!FileDict.TryGetValue(filename, out FileEntry file)) - { - throw new FileNotFoundException(); - } - - return OpenFile(file); + return SaveFs.OpenFile(filename); } public Stream OpenFile(FileEntry file) { - if (file.BlockIndex < 0) - { - return Stream.Null; - } - - return OpenFatBlock(file.BlockIndex, file.FileSize); + return SaveFs.OpenFile(file); } - private AllocationTableStream OpenFatBlock(int blockIndex, long size) - { - return new AllocationTableStream(IvfcStreamSource.CreateStream(), AllocationTable, (int)Header.Save.BlockSize, blockIndex, size); - } - public bool FileExists(string filename) => FileDict.ContainsKey(filename); - - private void ReadFileInfo() - { - // todo: Query the FAT for the file size when none is given - AllocationTableStream dirTableStream = OpenFatBlock(Header.Save.DirectoryTableBlock, 1000000); - AllocationTableStream fileTableStream = OpenFatBlock(Header.Save.FileTableBlock, 1000000); - - DirectoryEntry[] dirEntries = ReadDirEntries(dirTableStream); - FileEntry[] fileEntries = ReadFileEntries(fileTableStream); - - foreach (DirectoryEntry dir in dirEntries) - { - if (dir.NextSiblingIndex != 0) dir.NextSibling = dirEntries[dir.NextSiblingIndex]; - if (dir.FirstChildIndex != 0) dir.FirstChild = dirEntries[dir.FirstChildIndex]; - if (dir.FirstFileIndex != 0) dir.FirstFile = fileEntries[dir.FirstFileIndex]; - if (dir.NextInChainIndex != 0) dir.NextInChain = dirEntries[dir.NextInChainIndex]; - if (dir.ParentDirIndex != 0 && dir.ParentDirIndex < dirEntries.Length) - dir.ParentDir = dirEntries[dir.ParentDirIndex]; - } - - foreach (FileEntry file in fileEntries) - { - if (file.NextSiblingIndex != 0) file.NextSibling = fileEntries[file.NextSiblingIndex]; - if (file.NextInChainIndex != 0) file.NextInChain = fileEntries[file.NextInChainIndex]; - if (file.ParentDirIndex != 0 && file.ParentDirIndex < dirEntries.Length) - file.ParentDir = dirEntries[file.ParentDirIndex]; - } - - RootDirectory = dirEntries[2]; - - FileEntry fileChain = fileEntries[1].NextInChain; - var files = new List(); - while (fileChain != null) - { - files.Add(fileChain); - fileChain = fileChain.NextInChain; - } - - DirectoryEntry dirChain = dirEntries[1].NextInChain; - var dirs = new List(); - while (dirChain != null) - { - dirs.Add(dirChain); - dirChain = dirChain.NextInChain; - } - - Files = files.ToArray(); - Directories = dirs.ToArray(); - - FsEntry.ResolveFilenames(Files); - FsEntry.ResolveFilenames(Directories); - } - - private FileEntry[] ReadFileEntries(Stream stream) - { - var reader = new BinaryReader(stream); - int count = reader.ReadInt32(); - - reader.BaseStream.Position -= 4; - - var entries = new FileEntry[count]; - for (int i = 0; i < count; i++) - { - entries[i] = new FileEntry(reader); - } - - return entries; - } - - private DirectoryEntry[] ReadDirEntries(Stream stream) - { - var reader = new BinaryReader(stream); - int count = reader.ReadInt32(); - - reader.BaseStream.Position -= 4; - - var entries = new DirectoryEntry[count]; - for (int i = 0; i < count; i++) - { - entries[i] = new DirectoryEntry(reader); - } - - return entries; - } + public bool FileExists(string filename) => SaveFs.FileExists(filename); public bool SignHeader(Keyset keyset) { From 4a43c330b6cc50510d4aad364ccef06d74a0f59e Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 15 Oct 2018 20:53:49 -0500 Subject: [PATCH 04/13] Add RemapStorage class --- LibHac/Save/Header.cs | 17 +++--- LibHac/Save/RemapStorage.cs | 116 ++++++++++++++++++++++++++++++++++++ LibHac/Save/RemapStream.cs | 42 +++---------- LibHac/Save/SaveFs.cs | 2 +- LibHac/Save/Savefile.cs | 111 +++++++++++++--------------------- hactoolnet/ProcessSave.cs | 44 ++++++++++---- 6 files changed, 208 insertions(+), 124 deletions(-) create mode 100644 LibHac/Save/RemapStorage.cs diff --git a/LibHac/Save/Header.cs b/LibHac/Save/Header.cs index e3f9fda6..17afa2f8 100644 --- a/LibHac/Save/Header.cs +++ b/LibHac/Save/Header.cs @@ -192,11 +192,11 @@ namespace LibHac.Save public class RemapHeader { - public string Magic { get; set; } - public uint MagicNum { get; set; } - public int MapEntryCount { get; set; } - public int MapSegmentCount { get; set; } - public int Field10 { get; set; } + public string Magic { get; } + public uint MagicNum { get; } + public int MapEntryCount { get; } + public int MapSegmentCount { get; } + public int SegmentBits { get; } public RemapHeader(BinaryReader reader) { @@ -204,7 +204,7 @@ namespace LibHac.Save MagicNum = reader.ReadUInt32(); MapEntryCount = reader.ReadInt32(); MapSegmentCount = reader.ReadInt32(); - Field10 = reader.ReadInt32(); + SegmentBits = reader.ReadInt32(); } } @@ -320,7 +320,8 @@ namespace LibHac.Save public long PhysicalOffset { get; } public long Size { get; } public int Alignment { get; } - public int StorageType { get; } + public int Field1C { get; } + public long VirtualOffsetEnd => VirtualOffset + Size; public long PhysicalOffsetEnd => PhysicalOffset + Size; internal RemapSegment Segment { get; set; } @@ -332,7 +333,7 @@ namespace LibHac.Save PhysicalOffset = reader.ReadInt64(); Size = reader.ReadInt64(); Alignment = reader.ReadInt32(); - StorageType = reader.ReadInt32(); + Field1C = reader.ReadInt32(); } } diff --git a/LibHac/Save/RemapStorage.cs b/LibHac/Save/RemapStorage.cs new file mode 100644 index 00000000..e61cfa4b --- /dev/null +++ b/LibHac/Save/RemapStorage.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using LibHac.Streams; + +namespace LibHac.Save +{ + public class RemapStorage + { + private SharedStreamSource StreamSource { get; } + private RemapHeader Header { get; } + public MapEntry[] MapEntries { get; set; } + public RemapSegment[] Segments { get; set; } + + /// + /// Creates a new + /// + /// A of the main data of the RemapStream. + /// The object takes complete ownership of the Stream. + /// The header for this RemapStorage. + /// The remapping entries for this RemapStorage. + public RemapStorage(Stream data, RemapHeader header, MapEntry[] mapEntries) + { + StreamSource = new SharedStreamSource(data); + Header = header; + MapEntries = mapEntries; + + Segments = InitSegments(Header, MapEntries); + } + + public Stream OpenStream(long offset, long size) + { + int segmentIdx = GetSegmentFromVirtualOffset(offset); + long segmentOffset = GetOffsetFromVirtualOffset(offset); + + if (segmentIdx > Segments.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + RemapSegment segment = Segments[GetSegmentFromVirtualOffset(offset)]; + + if (segmentOffset > segment.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + Stream stream = new RemapStream(StreamSource.CreateStream(), segment); + + return new SubStream(stream, offset, size); + } + + public Stream OpenSegmentStream(int segment) + { + long offset = ToVirtualOffset(segment, 0); + long size = Segments[segment].Length; + + return OpenStream(offset, size); + } + + private static RemapSegment[] InitSegments(RemapHeader header, MapEntry[] mapEntries) + { + var segments = new RemapSegment[header.MapSegmentCount]; + int entryIdx = 0; + + for (int i = 0; i < header.MapSegmentCount; i++) + { + var seg = new RemapSegment(); + seg.Entries.Add(mapEntries[entryIdx]); + seg.Offset = mapEntries[entryIdx].VirtualOffset; + mapEntries[entryIdx].Segment = seg; + entryIdx++; + + while (entryIdx < mapEntries.Length && + mapEntries[entryIdx - 1].VirtualOffsetEnd == mapEntries[entryIdx].VirtualOffset) + { + mapEntries[entryIdx].Segment = seg; + mapEntries[entryIdx - 1].Next = mapEntries[entryIdx]; + seg.Entries.Add(mapEntries[entryIdx]); + entryIdx++; + } + + seg.Length = seg.Entries[seg.Entries.Count - 1].VirtualOffsetEnd - seg.Entries[0].VirtualOffset; + segments[i] = seg; + } + + return segments; + } + + private int GetSegmentFromVirtualOffset(long virtualOffset) + { + return (int)((ulong)virtualOffset >> (64 - Header.SegmentBits)); + } + + private long GetOffsetFromVirtualOffset(long virtualOffset) + { + return virtualOffset & GetOffsetMask(); + } + + private long ToVirtualOffset(int segment, long offset) + { + long seg = (segment << (64 - Header.SegmentBits)) & GetSegmentMask(); + long off = offset & GetOffsetMask(); + return seg | off; + } + + private long GetOffsetMask() + { + return (1 << (64 - Header.SegmentBits)) - 1; + } + + private long GetSegmentMask() + { + return ~GetOffsetMask(); + } + } +} diff --git a/LibHac/Save/RemapStream.cs b/LibHac/Save/RemapStream.cs index e1adab68..d74b12ec 100644 --- a/LibHac/Save/RemapStream.cs +++ b/LibHac/Save/RemapStream.cs @@ -9,40 +9,15 @@ namespace LibHac.Save { private long _position; private Stream BaseStream { get; } - public MapEntry[] MapEntries { get; set; } - public MapEntry CurrentEntry { get; set; } - public RemapSegment[] Segments { get; set; } + private RemapSegment Segment { get; } + private MapEntry CurrentEntry { get; set; } - public RemapStream(Stream baseStream, MapEntry[] entries, int segmentCount) + public RemapStream(Stream baseStream, RemapSegment segment) { BaseStream = baseStream; - MapEntries = entries; - Segments = new RemapSegment[segmentCount]; - - int entryIdx = 0; - for (int i = 0; i < segmentCount; i++) - { - var seg = new RemapSegment(); - seg.Entries.Add(MapEntries[entryIdx]); - seg.Offset = MapEntries[entryIdx].VirtualOffset; - MapEntries[entryIdx].Segment = seg; - entryIdx++; - - while (entryIdx < MapEntries.Length && - MapEntries[entryIdx - 1].VirtualOffsetEnd == MapEntries[entryIdx].VirtualOffset) - { - MapEntries[entryIdx].Segment = seg; - MapEntries[entryIdx - 1].Next = MapEntries[entryIdx]; - seg.Entries.Add(MapEntries[entryIdx]); - entryIdx++; - } - - seg.Length = seg.Entries[seg.Entries.Count - 1].VirtualOffsetEnd - seg.Entries[0].VirtualOffset; - Segments[i] = seg; - } - - CurrentEntry = GetMapEntry(0); - UpdateBaseStreamPosition(); + Segment = segment; + CurrentEntry = segment.Entries[0]; + Length = segment.Length; } public override int Read(byte[] buffer, int offset, int count) @@ -103,8 +78,7 @@ namespace LibHac.Save private MapEntry GetMapEntry(long offset) { - // todo: is O(n) search a possible performance issue? - MapEntry entry = MapEntries.FirstOrDefault(x => offset >= x.VirtualOffset && offset < x.VirtualOffsetEnd); + MapEntry entry = Segment.Entries.FirstOrDefault(x => offset >= x.VirtualOffset && offset < x.VirtualOffsetEnd); if (entry == null) throw new ArgumentOutOfRangeException(nameof(offset)); return entry; } @@ -120,7 +94,7 @@ namespace LibHac.Save public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; - public override long Length { get; } = -1; + public override long Length { get; } public override long Position { diff --git a/LibHac/Save/SaveFs.cs b/LibHac/Save/SaveFs.cs index 533db211..3ee0d7dd 100644 --- a/LibHac/Save/SaveFs.cs +++ b/LibHac/Save/SaveFs.cs @@ -6,8 +6,8 @@ namespace LibHac.Save { public class SaveFs { - private AllocationTable AllocationTable { get; } private SharedStreamSource StreamSource { get; } + private AllocationTable AllocationTable { get; } private SaveHeader Header { get; } public DirectoryEntry RootDirectory { get; private set; } diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index af3bd616..4135fb11 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -8,32 +8,18 @@ namespace LibHac.Save public class Savefile { public Header Header { get; } - private RemapStream FileRemap { get; } public SharedStreamSource SavefileSource { get; } - public SharedStreamSource FileRemapSource { get; } - private RemapStream MetaRemap { get; } - public SharedStreamSource MetaRemapSource { get; } + private JournalStream JournalStream { get; } public SharedStreamSource JournalStreamSource { get; } private HierarchicalIntegrityVerificationStream IvfcStream { get; } public SharedStreamSource IvfcStreamSource { get; } public SaveFs SaveFs { get; } - public Stream DuplexL1A { get; } - public Stream DuplexL1B { get; } - public Stream DuplexDataA { get; } - public Stream DuplexDataB { get; } - public LayeredDuplexFs DuplexData { get; } - public Stream JournalData { get; } + public RemapStorage DataRemapStorage { get; } + public RemapStorage MetaRemapStorage { get; } - public Stream JournalTable { get; } - public Stream JournalBitmapUpdatedPhysical { get; } - public Stream JournalBitmapUpdatedVirtual { get; } - public Stream JournalBitmapUnassigned { get; } - public Stream JournalLayer1Hash { get; } - public Stream JournalLayer2Hash { get; } - public Stream JournalLayer3Hash { get; } - public Stream JournalFat { get; } + public LayeredDuplexFs DuplexData { get; } public DirectoryEntry RootDirectory => SaveFs.RootDirectory; public FileEntry[] Files => SaveFs.Files; @@ -48,69 +34,59 @@ namespace LibHac.Save Header = new Header(keyset, reader); FsLayout layout = Header.Layout; - FileRemap = new RemapStream( - SavefileSource.CreateStream(layout.FileMapDataOffset, layout.FileMapDataSize), - Header.FileMapEntries, Header.FileRemap.MapSegmentCount); + DataRemapStorage = new RemapStorage(SavefileSource.CreateStream(layout.FileMapDataOffset, layout.FileMapDataSize), + Header.FileRemap, Header.FileMapEntries); - FileRemapSource = new SharedStreamSource(FileRemap); + DuplexData = InitDuplexStream(DataRemapStorage, Header); - var duplexLayers = new DuplexFsLayerInfo[3]; + MetaRemapStorage = new RemapStorage(DuplexData, Header.MetaRemap, Header.MetaMapEntries); - duplexLayers[0] = new DuplexFsLayerInfo - { - DataA = new MemoryStream(Header.DuplexMasterA), - DataB = new MemoryStream(Header.DuplexMasterB), - Info = Header.Duplex.Layers[0] - }; + Stream journalTable = MetaRemapStorage.OpenStream(layout.JournalTableOffset, layout.JournalTableSize); - duplexLayers[1] = new DuplexFsLayerInfo - { - DataA = FileRemapSource.CreateStream(layout.DuplexL1OffsetA, layout.DuplexL1Size), - DataB = FileRemapSource.CreateStream(layout.DuplexL1OffsetB, layout.DuplexL1Size), - Info = Header.Duplex.Layers[1] - }; + MappingEntry[] journalMap = JournalStream.ReadMappingEntries(journalTable, Header.Journal.MainDataBlockCount); - duplexLayers[2] = new DuplexFsLayerInfo - { - DataA = FileRemapSource.CreateStream(layout.DuplexDataOffsetA, layout.DuplexDataSize), - DataB = FileRemapSource.CreateStream(layout.DuplexDataOffsetB, layout.DuplexDataSize), - Info = Header.Duplex.Layers[2] - }; - - DuplexL1A = FileRemapSource.CreateStream(layout.DuplexL1OffsetA, layout.DuplexL1Size); - DuplexL1B = FileRemapSource.CreateStream(layout.DuplexL1OffsetB, layout.DuplexL1Size); - DuplexDataA = FileRemapSource.CreateStream(layout.DuplexDataOffsetA, layout.DuplexDataSize); - DuplexDataB = FileRemapSource.CreateStream(layout.DuplexDataOffsetB, layout.DuplexDataSize); - JournalData = FileRemapSource.CreateStream(layout.JournalDataOffset, layout.JournalDataSizeB + layout.SizeReservedArea); - - DuplexData = new LayeredDuplexFs(duplexLayers, Header.Layout.DuplexIndex == 1); - MetaRemap = new RemapStream(DuplexData, Header.MetaMapEntries, Header.MetaRemap.MapSegmentCount); - MetaRemapSource = new SharedStreamSource(MetaRemap); - - JournalTable = MetaRemapSource.CreateStream(layout.JournalTableOffset, layout.JournalTableSize); - JournalBitmapUpdatedPhysical = MetaRemapSource.CreateStream(layout.JournalBitmapUpdatedPhysicalOffset, layout.JournalBitmapUpdatedPhysicalSize); - JournalBitmapUpdatedVirtual = MetaRemapSource.CreateStream(layout.JournalBitmapUpdatedVirtualOffset, layout.JournalBitmapUpdatedVirtualSize); - JournalBitmapUnassigned = MetaRemapSource.CreateStream(layout.JournalBitmapUnassignedOffset, layout.JournalBitmapUnassignedSize); - JournalLayer1Hash = MetaRemapSource.CreateStream(layout.IvfcL1Offset, layout.IvfcL1Size); - JournalLayer2Hash = MetaRemapSource.CreateStream(layout.IvfcL2Offset, layout.IvfcL2Size); - JournalLayer3Hash = MetaRemapSource.CreateStream(layout.IvfcL3Offset, layout.IvfcL3Size); - JournalFat = MetaRemapSource.CreateStream(layout.FatOffset, layout.FatSize); - - MappingEntry[] journalMap = JournalStream.ReadMappingEntries(JournalTable, Header.Journal.MainDataBlockCount); - - SharedStream journalData = FileRemapSource.CreateStream(layout.JournalDataOffset, + Stream journalData = DataRemapStorage.OpenStream(layout.JournalDataOffset, layout.JournalDataSizeB + layout.SizeReservedArea); JournalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); JournalStreamSource = new SharedStreamSource(JournalStream); IvfcStream = InitIvfcStream(integrityCheckLevel); - SaveFs = new SaveFs(IvfcStream, MetaRemapSource.CreateStream(layout.FatOffset, layout.FatSize), Header.Save); + SaveFs = new SaveFs(IvfcStream, MetaRemapStorage.OpenStream(layout.FatOffset, layout.FatSize), Header.Save); IvfcStreamSource = new SharedStreamSource(IvfcStream); } } + private static LayeredDuplexFs InitDuplexStream(RemapStorage baseStorage, Header header) + { + FsLayout layout = header.Layout; + var duplexLayers = new DuplexFsLayerInfo[3]; + + duplexLayers[0] = new DuplexFsLayerInfo + { + DataA = new MemoryStream(header.DuplexMasterA), + DataB = new MemoryStream(header.DuplexMasterB), + Info = header.Duplex.Layers[0] + }; + + duplexLayers[1] = new DuplexFsLayerInfo + { + DataA = baseStorage.OpenStream(layout.DuplexL1OffsetA, layout.DuplexL1Size), + DataB = baseStorage.OpenStream(layout.DuplexL1OffsetB, layout.DuplexL1Size), + Info = header.Duplex.Layers[1] + }; + + duplexLayers[2] = new DuplexFsLayerInfo + { + DataA = baseStorage.OpenStream(layout.DuplexDataOffsetA, layout.DuplexDataSize), + DataB = baseStorage.OpenStream(layout.DuplexDataOffsetB, layout.DuplexDataSize), + Info = header.Duplex.Layers[2] + }; + + return new LayeredDuplexFs(duplexLayers, layout.DuplexIndex == 1); + } + private HierarchicalIntegrityVerificationStream InitIvfcStream(IntegrityCheckLevel integrityCheckLevel) { IvfcHeader ivfc = Header.Ivfc; @@ -130,8 +106,8 @@ namespace LibHac.Save IvfcLevelHeader level = ivfc.LevelHeaders[i - 1]; Stream data = i == ivfcLevels - 1 - ? (Stream)JournalStream - : MetaRemapSource.CreateStream(level.LogicalOffset, level.HashDataSize); + ? JournalStream + : MetaRemapStorage.OpenStream(level.LogicalOffset, level.HashDataSize); initInfo[i] = new IntegrityVerificationInfo { @@ -155,7 +131,6 @@ namespace LibHac.Save return SaveFs.OpenFile(file); } - public bool FileExists(string filename) => SaveFs.FileExists(filename); public bool SignHeader(Keyset keyset) diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index 10d103b5..12b9a931 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -26,25 +26,43 @@ namespace hactoolnet string dir = ctx.Options.DebugOutDir; Directory.CreateDirectory(dir); + FsLayout layout = save.Header.Layout; + File.WriteAllBytes(Path.Combine(dir, "L0_0_MasterHashA"), save.Header.MasterHashA); File.WriteAllBytes(Path.Combine(dir, "L0_1_MasterHashB"), save.Header.MasterHashB); File.WriteAllBytes(Path.Combine(dir, "L0_2_DuplexMasterA"), save.Header.DuplexMasterA); File.WriteAllBytes(Path.Combine(dir, "L0_3_DuplexMasterB"), save.Header.DuplexMasterB); - save.DuplexL1A.WriteAllBytes(Path.Combine(dir, "L0_4_DuplexL1A"), ctx.Logger); - save.DuplexL1B.WriteAllBytes(Path.Combine(dir, "L0_5_DuplexL1B"), ctx.Logger); - save.DuplexDataA.WriteAllBytes(Path.Combine(dir, "L0_6_DuplexDataA"), ctx.Logger); - save.DuplexDataB.WriteAllBytes(Path.Combine(dir, "L0_7_DuplexDataB"), ctx.Logger); - save.JournalData.WriteAllBytes(Path.Combine(dir, "L0_9_JournalData"), ctx.Logger); + Stream duplexL1A = save.DataRemapStorage.OpenStream(layout.DuplexL1OffsetA, layout.DuplexL1Size); + Stream duplexL1B = save.DataRemapStorage.OpenStream(layout.DuplexL1OffsetB, layout.DuplexL1Size); + Stream duplexDataA = save.DataRemapStorage.OpenStream(layout.DuplexDataOffsetA, layout.DuplexDataSize); + Stream duplexDataB = save.DataRemapStorage.OpenStream(layout.DuplexDataOffsetB, layout.DuplexDataSize); + Stream journalData = save.DataRemapStorage.OpenStream(layout.JournalDataOffset, layout.JournalDataSizeB + layout.SizeReservedArea); + + duplexL1A.WriteAllBytes(Path.Combine(dir, "L0_4_DuplexL1A"), ctx.Logger); + duplexL1B.WriteAllBytes(Path.Combine(dir, "L0_5_DuplexL1B"), ctx.Logger); + duplexDataA.WriteAllBytes(Path.Combine(dir, "L0_6_DuplexDataA"), ctx.Logger); + duplexDataB.WriteAllBytes(Path.Combine(dir, "L0_7_DuplexDataB"), ctx.Logger); + journalData.WriteAllBytes(Path.Combine(dir, "L0_9_JournalData"), ctx.Logger); save.DuplexData.WriteAllBytes(Path.Combine(dir, "L1_0_DuplexData"), ctx.Logger); - save.JournalTable.WriteAllBytes(Path.Combine(dir, "L2_0_JournalTable"), ctx.Logger); - save.JournalBitmapUpdatedPhysical.WriteAllBytes(Path.Combine(dir, "L2_1_JournalBitmapUpdatedPhysical"), ctx.Logger); - save.JournalBitmapUpdatedVirtual.WriteAllBytes(Path.Combine(dir, "L2_2_JournalBitmapUpdatedVirtual"), ctx.Logger); - save.JournalBitmapUnassigned.WriteAllBytes(Path.Combine(dir, "L2_3_JournalBitmapUnassigned"), ctx.Logger); - save.JournalLayer1Hash.WriteAllBytes(Path.Combine(dir, "L2_4_Layer1Hash"), ctx.Logger); - save.JournalLayer2Hash.WriteAllBytes(Path.Combine(dir, "L2_5_Layer2Hash"), ctx.Logger); - save.JournalLayer3Hash.WriteAllBytes(Path.Combine(dir, "L2_6_Layer3Hash"), ctx.Logger); - save.JournalFat.WriteAllBytes(Path.Combine(dir, "L2_7_FAT"), ctx.Logger); + + Stream journalTable = save.MetaRemapStorage.OpenStream(layout.JournalTableOffset, layout.JournalTableSize); + Stream journalBitmapUpdatedPhysical = save.MetaRemapStorage.OpenStream(layout.JournalBitmapUpdatedPhysicalOffset, layout.JournalBitmapUpdatedPhysicalSize); + Stream journalBitmapUpdatedVirtual = save.MetaRemapStorage.OpenStream(layout.JournalBitmapUpdatedVirtualOffset, layout.JournalBitmapUpdatedVirtualSize); + Stream journalBitmapUnassigned = save.MetaRemapStorage.OpenStream(layout.JournalBitmapUnassignedOffset, layout.JournalBitmapUnassignedSize); + Stream journalLayer1Hash = save.MetaRemapStorage.OpenStream(layout.IvfcL1Offset, layout.IvfcL1Size); + Stream journalLayer2Hash = save.MetaRemapStorage.OpenStream(layout.IvfcL2Offset, layout.IvfcL2Size); + Stream journalLayer3Hash = save.MetaRemapStorage.OpenStream(layout.IvfcL3Offset, layout.IvfcL3Size); + Stream journalFat = save.MetaRemapStorage.OpenStream(layout.FatOffset, layout.FatSize); + + journalTable.WriteAllBytes(Path.Combine(dir, "L2_0_JournalTable"), ctx.Logger); + journalBitmapUpdatedPhysical.WriteAllBytes(Path.Combine(dir, "L2_1_JournalBitmapUpdatedPhysical"), ctx.Logger); + journalBitmapUpdatedVirtual.WriteAllBytes(Path.Combine(dir, "L2_2_JournalBitmapUpdatedVirtual"), ctx.Logger); + journalBitmapUnassigned.WriteAllBytes(Path.Combine(dir, "L2_3_JournalBitmapUnassigned"), ctx.Logger); + journalLayer1Hash.WriteAllBytes(Path.Combine(dir, "L2_4_Layer1Hash"), ctx.Logger); + journalLayer2Hash.WriteAllBytes(Path.Combine(dir, "L2_5_Layer2Hash"), ctx.Logger); + journalLayer3Hash.WriteAllBytes(Path.Combine(dir, "L2_6_Layer3Hash"), ctx.Logger); + journalFat.WriteAllBytes(Path.Combine(dir, "L2_7_FAT"), ctx.Logger); save.IvfcStreamSource.CreateStream().WriteAllBytes(Path.Combine(dir, "L3_0_SaveData"), ctx.Logger); } From 0eaefba0716434967c7db88d493bf3b940eefda2 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 15 Oct 2018 21:17:53 -0500 Subject: [PATCH 05/13] Make SaveFs writable --- LibHac/Save/AllocationTableStream.cs | 21 +++++++++++++++++++-- LibHac/Save/Savefile.cs | 7 +++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/LibHac/Save/AllocationTableStream.cs b/LibHac/Save/AllocationTableStream.cs index 49785789..9adb6884 100644 --- a/LibHac/Save/AllocationTableStream.cs +++ b/LibHac/Save/AllocationTableStream.cs @@ -76,12 +76,29 @@ namespace LibHac.Save public override void Write(byte[] buffer, int offset, int count) { - throw new NotImplementedException(); + int remaining = count; + int outOffset = offset; + + while (remaining > 0) + { + int remainingInSegment = Iterator.CurrentSegmentSize * BlockSize - SegmentPos; + int bytesToWrite = Math.Min(remaining, remainingInSegment); + Data.Write(buffer, outOffset, bytesToWrite); + + outOffset += bytesToWrite; + remaining -= bytesToWrite; + + if (SegmentPos >= Iterator.CurrentSegmentSize * BlockSize) + { + if (!Iterator.MoveNext()) return; + Data.Position = Iterator.PhysicalBlock * BlockSize; + } + } } public override bool CanRead => true; public override bool CanSeek => true; - public override bool CanWrite => false; + public override bool CanWrite => true; public override long Length { get; } public override long Position diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index 4135fb11..ba3394f0 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -10,7 +10,6 @@ namespace LibHac.Save public Header Header { get; } public SharedStreamSource SavefileSource { get; } - private JournalStream JournalStream { get; } public SharedStreamSource JournalStreamSource { get; } private HierarchicalIntegrityVerificationStream IvfcStream { get; } public SharedStreamSource IvfcStreamSource { get; } @@ -47,8 +46,8 @@ namespace LibHac.Save Stream journalData = DataRemapStorage.OpenStream(layout.JournalDataOffset, layout.JournalDataSizeB + layout.SizeReservedArea); - JournalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); - JournalStreamSource = new SharedStreamSource(JournalStream); + var journalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); + JournalStreamSource = new SharedStreamSource(journalStream); IvfcStream = InitIvfcStream(integrityCheckLevel); @@ -106,7 +105,7 @@ namespace LibHac.Save IvfcLevelHeader level = ivfc.LevelHeaders[i - 1]; Stream data = i == ivfcLevels - 1 - ? JournalStream + ? JournalStreamSource.CreateStream() : MetaRemapStorage.OpenStream(level.LogicalOffset, level.HashDataSize); initInfo[i] = new IntegrityVerificationInfo From c0393449f1e0719b0a72b449b3827e0bd61e3b08 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Tue, 16 Oct 2018 15:01:16 -0500 Subject: [PATCH 06/13] IVFS writing --- LibHac/IntegrityVerificationStream.cs | 52 ++++++++++++++-------- LibHac/Streams/RandomAccessSectorStream.cs | 2 +- LibHac/Streams/SectorStream.cs | 4 +- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/LibHac/IntegrityVerificationStream.cs b/LibHac/IntegrityVerificationStream.cs index c8d72f46..4c07f700 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -90,7 +90,7 @@ namespace LibHac bytesToHash = bytesRead; } } - + if (BlockValidities[blockNum] == Validity.Invalid && integrityCheckLevel == IntegrityCheckLevel.ErrorOnInvalid) { throw new InvalidDataException("Hash error!"); @@ -100,23 +100,7 @@ namespace LibHac if (BlockValidities[blockNum] != Validity.Unchecked) return bytesRead; - _hash.Initialize(); - - if (Type == IntegrityStreamType.Save) - { - _hash.TransformBlock(Salt, 0, Salt.Length, null, 0); - } - - _hash.TransformBlock(buffer, offset, bytesToHash, null, 0); - _hash.TransformFinalBlock(buffer, 0, 0); - - byte[] hash = _hash.Hash; - - if (Type == IntegrityStreamType.Save) - { - // This bit is set on all save hashes - hash[0x1F] |= 0x80; - } + byte[] hash = DoHash(buffer, offset, bytesToHash); Validity validity = Util.ArraysEqual(_hashBuffer, hash) ? Validity.Valid : Validity.Invalid; BlockValidities[blockNum] = validity; @@ -131,7 +115,37 @@ namespace LibHac public override void Write(byte[] buffer, int offset, int count) { - throw new NotImplementedException(); + long blockNum = CurrentSector; + int toWrite = (int)Math.Min(count, Length - Position); + byte[] hash = DoHash(buffer, offset, toWrite); + + base.Write(buffer, offset, count); + + HashStream.Position = blockNum * DigestSize; + HashStream.Write(hash, 0, DigestSize); + } + + private byte[] DoHash(byte[] buffer, int offset, int count) + { + _hash.Initialize(); + + if (Type == IntegrityStreamType.Save) + { + _hash.TransformBlock(Salt, 0, Salt.Length, null, 0); + } + + _hash.TransformBlock(buffer, offset, count, null, 0); + _hash.TransformFinalBlock(buffer, 0, 0); + + byte[] hash = _hash.Hash; + + if (Type == IntegrityStreamType.Save) + { + // This bit is set on all save hashes + hash[0x1F] |= 0x80; + } + + return hash; } public override bool CanRead => true; diff --git a/LibHac/Streams/RandomAccessSectorStream.cs b/LibHac/Streams/RandomAccessSectorStream.cs index 29ccda27..dc56f5ce 100644 --- a/LibHac/Streams/RandomAccessSectorStream.cs +++ b/LibHac/Streams/RandomAccessSectorStream.cs @@ -44,7 +44,7 @@ namespace LibHac.Streams if (_readBytes == 0 || !_bufferDirty) return; _baseStream.Position = _currentSector * _bufferSize; - _baseStream.Write(_buffer, 0, _readBytes); + _baseStream.Write(_buffer, 0, _bufferSize); _readBytes = 0; _bufferDirty = false; diff --git a/LibHac/Streams/SectorStream.cs b/LibHac/Streams/SectorStream.cs index c69d42be..abf7f93f 100644 --- a/LibHac/Streams/SectorStream.cs +++ b/LibHac/Streams/SectorStream.cs @@ -112,7 +112,9 @@ namespace LibHac.Streams public override void Write(byte[] buffer, int offset, int count) { ValidateSize(count); - _baseStream.Write(buffer, offset, count); + int toWrite = (int)Math.Min(count, Length - Position); + + _baseStream.Write(buffer, offset, toWrite); CurrentSector += count / SectorSize; } From da5eec1b3d7b28d8a71d638d286893cb8335e134 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Wed, 17 Oct 2018 17:15:57 -0500 Subject: [PATCH 07/13] Writing for all save file layers --- ...HierarchicalIntegrityVerificationStream.cs | 4 +-- LibHac/IntegrityVerificationStream.cs | 16 ++++++--- LibHac/Save/AllocationTableStream.cs | 8 ++++- LibHac/Save/DuplexFs.cs | 33 +++++++++++++++---- LibHac/Save/Journal.cs | 29 ++++++++++++++-- LibHac/Save/LayeredDuplexFs.cs | 4 +-- LibHac/Save/RemapStream.cs | 28 ++++++++++++---- LibHac/Streams/RandomAccessSectorStream.cs | 2 ++ LibHac/Streams/SharedStreamSource.cs | 8 ++++- LibHac/Streams/SubStream.cs | 15 +++++---- hactoolnet/ProcessSave.cs | 2 +- 11 files changed, 116 insertions(+), 33 deletions(-) diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index c6b2d9d5..dfdb3f2c 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -82,7 +82,7 @@ namespace LibHac public override void Flush() { - throw new NotImplementedException(); + DataLevel.Flush(); } public override long Seek(long offset, SeekOrigin origin) @@ -115,7 +115,7 @@ namespace LibHac public override void Write(byte[] buffer, int offset, int count) { - throw new NotImplementedException(); + DataLevel.Write(buffer, offset, count); } public override bool CanRead => DataLevel.CanRead; diff --git a/LibHac/IntegrityVerificationStream.cs b/LibHac/IntegrityVerificationStream.cs index 4c07f700..397a2c4b 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -30,11 +30,6 @@ namespace LibHac BlockValidities = new Validity[SectorCount]; } - public override void Flush() - { - throw new NotImplementedException(); - } - public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -119,6 +114,11 @@ namespace LibHac int toWrite = (int)Math.Min(count, Length - Position); byte[] hash = DoHash(buffer, offset, toWrite); + if (Type == IntegrityStreamType.Save && buffer.IsEmpty()) + { + Array.Clear(hash, 0, DigestSize); + } + base.Write(buffer, offset, count); HashStream.Position = blockNum * DigestSize; @@ -148,6 +148,12 @@ namespace LibHac return hash; } + public override void Flush() + { + HashStream.Flush(); + base.Flush(); + } + public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; diff --git a/LibHac/Save/AllocationTableStream.cs b/LibHac/Save/AllocationTableStream.cs index 9adb6884..e7acf015 100644 --- a/LibHac/Save/AllocationTableStream.cs +++ b/LibHac/Save/AllocationTableStream.cs @@ -22,7 +22,7 @@ namespace LibHac.Save public override void Flush() { - throw new NotImplementedException(); + Data.Flush(); } public override int Read(byte[] buffer, int offset, int count) @@ -125,5 +125,11 @@ namespace LibHac.Save Data.Position = Iterator.PhysicalBlock * BlockSize + segmentPos; } } + + protected override void Dispose(bool disposing) + { + Flush(); + base.Dispose(disposing); + } } } diff --git a/LibHac/Save/DuplexFs.cs b/LibHac/Save/DuplexFs.cs index 86282822..7e266f90 100644 --- a/LibHac/Save/DuplexFs.cs +++ b/LibHac/Save/DuplexFs.cs @@ -28,7 +28,9 @@ namespace LibHac.Save public override void Flush() { - throw new NotImplementedException(); + BitmapStream?.Flush(); + DataA?.Flush(); + DataB?.Flush(); } public override int Read(byte[] buffer, int offset, int count) @@ -57,6 +59,30 @@ namespace LibHac.Save return totalBytesRead; } + public override void Write(byte[] buffer, int offset, int count) + { + long remaining = Math.Min(count, Length - Position); + if (remaining <= 0) return; + + int inOffset = offset; + + while (remaining > 0) + { + int blockNum = (int)(Position / BlockSize); + int blockPos = (int)(Position % BlockSize); + int bytesToWrite = (int)Math.Min(remaining, BlockSize - blockPos); + + Stream data = Bitmap.Bitmap[blockNum] ? DataB : DataA; + data.Position = blockNum * BlockSize + blockPos; + + data.Write(buffer, inOffset, bytesToWrite); + + inOffset += bytesToWrite; + remaining -= bytesToWrite; + Position += bytesToWrite; + } + } + public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -80,11 +106,6 @@ namespace LibHac.Save throw new NotImplementedException(); } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; diff --git a/LibHac/Save/Journal.cs b/LibHac/Save/Journal.cs index 23c8dea3..ed942cea 100644 --- a/LibHac/Save/Journal.cs +++ b/LibHac/Save/Journal.cs @@ -45,6 +45,25 @@ namespace LibHac.Save return count; } + public override void Write(byte[] buffer, int offset, int count) + { + long remaining = Math.Min(Length - Position, count); + if (remaining <= 0) return; + + int inPos = offset; + + while (remaining > 0) + { + long remainInEntry = BlockSize - Position % BlockSize; + int toRead = (int)Math.Min(remaining, remainInEntry); + BaseStream.Write(buffer, inPos, toRead); + + inPos += toRead; + remaining -= toRead; + Position += toRead; + } + } + public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -64,11 +83,15 @@ namespace LibHac.Save } 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 void Flush() + { + BaseStream.Flush(); + } + public override bool CanRead => true; public override bool CanSeek => true; - public override bool CanWrite => false; + public override bool CanWrite => true; public override long Length { get; } public override long Position { diff --git a/LibHac/Save/LayeredDuplexFs.cs b/LibHac/Save/LayeredDuplexFs.cs index 7042eb0f..795c743b 100644 --- a/LibHac/Save/LayeredDuplexFs.cs +++ b/LibHac/Save/LayeredDuplexFs.cs @@ -32,7 +32,7 @@ namespace LibHac.Save public override void Flush() { - throw new System.NotImplementedException(); + DataLayer.Flush(); } public override int Read(byte[] buffer, int offset, int count) @@ -52,7 +52,7 @@ namespace LibHac.Save public override void Write(byte[] buffer, int offset, int count) { - throw new System.NotImplementedException(); + DataLayer.Write(buffer, offset, count); } public override bool CanRead => DataLayer.CanRead; diff --git a/LibHac/Save/RemapStream.cs b/LibHac/Save/RemapStream.cs index d74b12ec..9486dde6 100644 --- a/LibHac/Save/RemapStream.cs +++ b/LibHac/Save/RemapStream.cs @@ -43,6 +43,27 @@ namespace LibHac.Save return count; } + public override void Write(byte[] buffer, int offset, int count) + { + if (CurrentEntry == null) throw new EndOfStreamException(); + + long remaining = Math.Min(CurrentEntry.VirtualOffsetEnd - Position, count); + if (remaining <= 0) return; + + int inPos = offset; + + while (remaining > 0) + { + long remainInEntry = CurrentEntry.VirtualOffsetEnd - Position; + int toWrite = (int)Math.Min(remaining, remainInEntry); + BaseStream.Write(buffer, inPos, toWrite); + + inPos += toWrite; + remaining -= toWrite; + Position += toWrite; + } + } + public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -66,14 +87,9 @@ namespace LibHac.Save throw new NotSupportedException(); } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - public override void Flush() { - throw new NotImplementedException(); + BaseStream.Flush(); } private MapEntry GetMapEntry(long offset) diff --git a/LibHac/Streams/RandomAccessSectorStream.cs b/LibHac/Streams/RandomAccessSectorStream.cs index dc56f5ce..98b31804 100644 --- a/LibHac/Streams/RandomAccessSectorStream.cs +++ b/LibHac/Streams/RandomAccessSectorStream.cs @@ -46,6 +46,7 @@ namespace LibHac.Streams _baseStream.Position = _currentSector * _bufferSize; _baseStream.Write(_buffer, 0, _bufferSize); + _bufferPos = 0; _readBytes = 0; _bufferDirty = false; } @@ -53,6 +54,7 @@ namespace LibHac.Streams public override void Flush() { WriteSectorIfDirty(); + _baseStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) diff --git a/LibHac/Streams/SharedStreamSource.cs b/LibHac/Streams/SharedStreamSource.cs index 471669e6..49055161 100644 --- a/LibHac/Streams/SharedStreamSource.cs +++ b/LibHac/Streams/SharedStreamSource.cs @@ -32,7 +32,13 @@ namespace LibHac.Streams return new SharedStream(this, offset, length); } - public void Flush() => BaseStream.Flush(); + public void Flush() + { + lock (Locker) + { + BaseStream.Flush(); + } + } public int Read(long readOffset, byte[] buffer, int bufferOffset, int count) { diff --git a/LibHac/Streams/SubStream.cs b/LibHac/Streams/SubStream.cs index a4c03ad3..fb77e244 100644 --- a/LibHac/Streams/SubStream.cs +++ b/LibHac/Streams/SubStream.cs @@ -21,7 +21,7 @@ namespace LibHac.Streams baseStream.Seek(offset, SeekOrigin.Begin); } - public SubStream(Stream baseStream, long offset) + public SubStream(Stream baseStream, long offset) : this(baseStream, offset, baseStream.Length - offset) { } public override int Read(byte[] buffer, int offset, int count) @@ -32,6 +32,14 @@ namespace LibHac.Streams return BaseStream.Read(buffer, offset, count); } + public override void Write(byte[] buffer, int offset, int count) + { + long remaining = Math.Min(Length - Position, count); + if (remaining <= 0) return; + + BaseStream.Write(buffer, offset, (int)remaining); + } + public override long Length { get; } public override bool CanRead => BaseStream.CanRead; public override bool CanWrite => BaseStream.CanWrite; @@ -70,10 +78,5 @@ namespace LibHac.Streams public override void SetLength(long value) => throw new NotSupportedException(); public override void Flush() => BaseStream.Flush(); - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } } } diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index 12b9a931..3d5743ce 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -90,7 +90,7 @@ namespace hactoolnet sb.AppendLine(); sb.AppendLine("Savefile:"); - PrintItem(sb, colLen, "CMAC Signature:", save.Header.Cmac); + PrintItem(sb, colLen, $"CMAC Signature{save.Header.SignatureValidity.GetValidityString()}:", save.Header.Cmac); PrintItem(sb, colLen, "Title ID:", $"{save.Header.ExtraData.TitleId:x16}"); PrintItem(sb, colLen, "User ID:", save.Header.ExtraData.UserId); PrintItem(sb, colLen, "Save ID:", $"{save.Header.ExtraData.SaveId:x16}"); From f8e7c00ef4e062c707888da375282128b127a1c6 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 18 Oct 2018 16:10:51 -0500 Subject: [PATCH 08/13] Tweak how IVFC validation is done --- ...HierarchicalIntegrityVerificationStream.cs | 28 ++++++----- LibHac/Nca.cs | 48 +++++++++++++------ 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index dfdb3f2c..64c0f5f2 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -39,31 +39,33 @@ namespace LibHac } /// - /// Checks the hashes of any unchecked blocks and returns the of the hash level. + /// Checks the hashes of any unchecked blocks and returns the of the data. /// - /// The level of hierarchical hashes to check. /// If , return as soon as an invalid block is found. /// An optional for reporting progress. /// The of the data of the specified hash level. - public Validity ValidateLevel(int level, bool returnOnError, IProgressReport logger = null) + public Validity Validate(bool returnOnError, IProgressReport logger = null) { - Validity[] validities = LevelValidities[level]; - IntegrityVerificationStream levelStream = IntegrityStreams[level]; + Validity[] validities = LevelValidities[LevelValidities.Length - 1]; + IntegrityVerificationStream stream = IntegrityStreams[IntegrityStreams.Length - 1]; - // The original position of the stream must be restored when we're done validating - long initialPosition = levelStream.Position; + // Restore the original position of the stream when we're done validating + long initialPosition = stream.Position; - var buffer = new byte[levelStream.SectorSize]; + long blockSize = stream.SectorSize; + int blockCount = (int)Util.DivideByRoundUp(Length, blockSize); + + var buffer = new byte[blockSize]; var result = Validity.Valid; - logger?.SetTotal(levelStream.SectorCount); + logger?.SetTotal(blockCount); - for (int i = 0; i < levelStream.SectorCount; i++) + for (int i = 0; i < blockCount; i++) { if (validities[i] == Validity.Unchecked) { - levelStream.Position = (long)levelStream.SectorSize * i; - levelStream.Read(buffer, 0, buffer.Length, IntegrityCheckLevel.IgnoreOnInvalid); + stream.Position = blockSize * i; + stream.Read(buffer, 0, buffer.Length, IntegrityCheckLevel.IgnoreOnInvalid); } if (validities[i] == Validity.Invalid) @@ -76,7 +78,7 @@ namespace LibHac } logger?.SetTotal(0); - levelStream.Position = initialPosition; + stream.Position = initialPosition; return result; } diff --git a/LibHac/Nca.cs b/LibHac/Nca.cs index 26477910..a6eb1b17 100644 --- a/LibHac/Nca.cs +++ b/LibHac/Nca.cs @@ -489,25 +489,43 @@ namespace LibHac if (stream == null) return Validity.Unchecked; if (!quiet) logger?.LogMessage($"Verifying section {index}..."); + Validity validity = stream.Validate(true, logger); - for (int i = 0; i < stream.Levels.Length - 1; i++) + if (hashType == NcaHashType.Ivfc) { - if (!quiet) logger?.LogMessage($" Verifying Hash Level {i}..."); - Validity levelValidity = stream.ValidateLevel(i, true, logger); - - if (hashType == NcaHashType.Ivfc) - { - sect.Header.IvfcInfo.LevelHeaders[i].HashValidity = levelValidity; - } - else if (hashType == NcaHashType.Sha256 && i == stream.Levels.Length - 2) - { - sect.Header.Sha256Info.HashValidity = levelValidity; - } - - if (levelValidity == Validity.Invalid) return Validity.Invalid; + SetIvfcLevelValidities(stream, sect.Header.IvfcInfo); + } + else if (hashType == NcaHashType.Sha256) + { + sect.Header.Sha256Info.HashValidity = validity; } - return Validity.Valid; + return validity; + } + + private static void SetIvfcLevelValidities(HierarchicalIntegrityVerificationStream stream, IvfcHeader header) + { + for (int i = 0; i < stream.Levels.Length - 1; i++) + { + Validity[] level = stream.LevelValidities[i]; + var levelValidity = Validity.Valid; + + foreach (Validity block in level) + { + if (block == Validity.Invalid) + { + levelValidity = Validity.Invalid; + break; + } + + if (block == Validity.Unchecked && levelValidity != Validity.Invalid) + { + levelValidity = Validity.Unchecked; + } + } + + header.LevelHeaders[i].HashValidity = levelValidity; + } } } } From f83a284b966a0288ad9dc99bb466a3eea1405acf Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 18 Oct 2018 17:54:24 -0500 Subject: [PATCH 09/13] Add verification option to save files --- LibHac/Aes128CtrStream.cs | 5 --- ...HierarchicalIntegrityVerificationStream.cs | 28 ++++++++++++ LibHac/IntegrityVerificationStream.cs | 1 + LibHac/Nca.cs | 44 +++++++------------ LibHac/NcaStructs.cs | 13 +++--- LibHac/Save/Savefile.cs | 8 ++++ hactoolnet/CliParser.cs | 4 +- hactoolnet/Options.cs | 12 ++++- hactoolnet/Print.cs | 40 ++++++++++++++++- hactoolnet/ProcessNca.cs | 28 +----------- hactoolnet/ProcessSave.cs | 24 +++++----- 11 files changed, 127 insertions(+), 80 deletions(-) diff --git a/LibHac/Aes128CtrStream.cs b/LibHac/Aes128CtrStream.cs index 382251b2..9c48c2a1 100644 --- a/LibHac/Aes128CtrStream.cs +++ b/LibHac/Aes128CtrStream.cs @@ -105,11 +105,6 @@ namespace LibHac Counter[8] = (byte)((Counter[8] & 0xF0) | (int)(off & 0x0F)); } - public override void Flush() - { - throw new NotImplementedException(); - } - public override long Seek(long offset, SeekOrigin origin) { switch (origin) diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index 64c0f5f2..dafed09e 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -130,4 +130,32 @@ namespace LibHac set => DataLevel.Position = value; } } + + public static class HierarchicalIntegrityVerificationStreamExtensions + { + internal static void SetLevelValidities(this HierarchicalIntegrityVerificationStream stream, IvfcHeader header) + { + for (int i = 0; i < stream.Levels.Length - 1; i++) + { + Validity[] level = stream.LevelValidities[i]; + var levelValidity = Validity.Valid; + + foreach (Validity block in level) + { + if (block == Validity.Invalid) + { + levelValidity = Validity.Invalid; + break; + } + + if (block == Validity.Unchecked && levelValidity != Validity.Invalid) + { + levelValidity = Validity.Unchecked; + } + } + + header.LevelHeaders[i].HashValidity = levelValidity; + } + } + } } diff --git a/LibHac/IntegrityVerificationStream.cs b/LibHac/IntegrityVerificationStream.cs index 397a2c4b..65e73295 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -71,6 +71,7 @@ namespace LibHac if (Type == IntegrityStreamType.Save && _hashBuffer.IsEmpty()) { Array.Clear(buffer, offset, SectorSize); + BlockValidities[blockNum] = Validity.Valid; return bytesRead; } diff --git a/LibHac/Nca.cs b/LibHac/Nca.cs index a6eb1b17..77b4105b 100644 --- a/LibHac/Nca.cs +++ b/LibHac/Nca.cs @@ -381,7 +381,6 @@ namespace LibHac stream.Read(hashTable, 0, hashTable.Length); sect.MasterHashValidity = Crypto.CheckMemoryHashTable(hashTable, expected, 0, hashTable.Length); - if (sect.Header.HashType == NcaHashType.Ivfc) sect.Header.IvfcInfo.LevelHeaders[0].HashValidity = sect.MasterHashValidity; } public void Dispose() @@ -400,7 +399,21 @@ namespace LibHac public int SectionNum { get; set; } public long Offset { get; set; } public long Size { get; set; } - public Validity MasterHashValidity { get; set; } + + public Validity MasterHashValidity + { + get + { + if (Header.HashType == NcaHashType.Ivfc) return Header.IvfcInfo.LevelHeaders[0].HashValidity; + if (Header.HashType == NcaHashType.Sha256) return Header.Sha256Info.MasterHashValidity; + return Validity.Unchecked; + } + set + { + if (Header.HashType == NcaHashType.Ivfc) Header.IvfcInfo.LevelHeaders[0].HashValidity = value; + if (Header.HashType == NcaHashType.Sha256) Header.Sha256Info.MasterHashValidity = value; + } + } public byte[] GetMasterHash() { @@ -493,7 +506,7 @@ namespace LibHac if (hashType == NcaHashType.Ivfc) { - SetIvfcLevelValidities(stream, sect.Header.IvfcInfo); + stream.SetLevelValidities(sect.Header.IvfcInfo); } else if (hashType == NcaHashType.Sha256) { @@ -502,30 +515,5 @@ namespace LibHac return validity; } - - private static void SetIvfcLevelValidities(HierarchicalIntegrityVerificationStream stream, IvfcHeader header) - { - for (int i = 0; i < stream.Levels.Length - 1; i++) - { - Validity[] level = stream.LevelValidities[i]; - var levelValidity = Validity.Valid; - - foreach (Validity block in level) - { - if (block == Validity.Invalid) - { - levelValidity = Validity.Invalid; - break; - } - - if (block == Validity.Unchecked && levelValidity != Validity.Invalid) - { - levelValidity = Validity.Unchecked; - } - } - - header.LevelHeaders[i].HashValidity = levelValidity; - } - } } } diff --git a/LibHac/NcaStructs.cs b/LibHac/NcaStructs.cs index c75f1c68..9ed3c0ee 100644 --- a/LibHac/NcaStructs.cs +++ b/LibHac/NcaStructs.cs @@ -158,8 +158,8 @@ namespace LibHac { public string Magic; public int Version; - public uint MasterHashSize; - public uint NumLevels; + public int MasterHashSize; + public int NumLevels; public IvfcLevelHeader[] LevelHeaders = new IvfcLevelHeader[6]; public byte[] SaltSource; public byte[] MasterHash; @@ -167,10 +167,10 @@ namespace LibHac public IvfcHeader(BinaryReader reader) { Magic = reader.ReadAscii(4); - Version = reader.ReadInt16(); reader.BaseStream.Position += 2; - MasterHashSize = reader.ReadUInt32(); - NumLevels = reader.ReadUInt32(); + Version = reader.ReadInt16(); + MasterHashSize = reader.ReadInt32(); + NumLevels = reader.ReadInt32(); for (int i = 0; i < LevelHeaders.Length; i++) { @@ -210,8 +210,9 @@ namespace LibHac public long DataOffset; public long DataSize; + public Validity MasterHashValidity = Validity.Unchecked; public Validity HashValidity = Validity.Unchecked; - + public Sha256Info(BinaryReader reader) { MasterHash = reader.ReadBytes(0x20); diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index ba3394f0..3ab2f999 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -151,6 +151,14 @@ namespace LibHac.Save return true; } + public Validity Verify(IProgressReport logger = null) + { + Validity validity = IvfcStream.Validate(true, logger); + IvfcStream.SetLevelValidities(Header.Ivfc); + + return validity; + } + private string[] SaltSources = { "HierarchicalIntegrityVerificationStorage::Master", diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index d2a434d4..089eb094 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -44,6 +44,7 @@ namespace hactoolnet new CliOption("listapps", 0, (o, a) => o.ListApps = true), new CliOption("listtitles", 0, (o, a) => o.ListTitles = true), new CliOption("listromfs", 0, (o, a) => o.ListRomFs = true), + new CliOption("listfiles", 0, (o, a) => o.ListFiles = true), new CliOption("sign", 0, (o, a) => o.SignSave = true), new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), }; @@ -203,9 +204,10 @@ namespace hactoolnet sb.AppendLine(" --outdir Specify directory path to save contents to."); sb.AppendLine(" --debugoutdir Specify directory path to save intermediate data to for debugging."); sb.AppendLine(" --sign Sign the save file. (Requires device_key in key file)"); + sb.AppendLine(" --listfiles List files in save file."); sb.AppendLine("Keygen options:"); sb.AppendLine(" --outdir Specify directory path to save key files to."); - + return sb.ToString(); } diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index 69d4aa30..2f8c77a9 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -34,11 +34,19 @@ namespace hactoolnet public bool ListApps; public bool ListTitles; public bool ListRomFs; + public bool ListFiles; public bool SignSave; public ulong TitleId; - public IntegrityCheckLevel IntegrityLevel => - EnableHash ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; + public IntegrityCheckLevel IntegrityLevel + { + get + { + if (Validate) return IntegrityCheckLevel.IgnoreOnInvalid; + if (EnableHash) return IntegrityCheckLevel.ErrorOnInvalid; + return IntegrityCheckLevel.None; + } + } } internal enum FileType diff --git a/hactoolnet/Print.cs b/hactoolnet/Print.cs index f9df4879..e0a1821d 100644 --- a/hactoolnet/Print.cs +++ b/hactoolnet/Print.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using LibHac; namespace hactoolnet @@ -26,5 +27,42 @@ namespace hactoolnet default: return string.Empty; } } + + public static void PrintIvfcHash(StringBuilder sb, int colLen, int indentSize, IvfcHeader ivfcInfo, IntegrityStreamType type) + { + string prefix = new string(' ', indentSize); + string prefix2 = new string(' ', indentSize + 4); + + if (type == IntegrityStreamType.RomFs) + PrintItem(sb, colLen, $"{prefix}Master Hash{ivfcInfo.LevelHeaders[0].HashValidity.GetValidityString()}:", ivfcInfo.MasterHash); + + PrintItem(sb, colLen, $"{prefix}Magic:", ivfcInfo.Magic); + PrintItem(sb, colLen, $"{prefix}Version:", ivfcInfo.Version); + + if (type == IntegrityStreamType.Save) + PrintItem(sb, colLen, $"{prefix}Salt Seed:", ivfcInfo.SaltSource); + + int levelCount = Math.Max(ivfcInfo.NumLevels - 1, 0); + if (type == IntegrityStreamType.Save) levelCount = 4; + + int offsetLen = type == IntegrityStreamType.Save ? 16 : 12; + + for (int i = 0; i < levelCount; i++) + { + IvfcLevelHeader level = ivfcInfo.LevelHeaders[i]; + long hashOffset = 0; + + if (i != 0) + { + hashOffset = ivfcInfo.LevelHeaders[i - 1].LogicalOffset; + } + + sb.AppendLine($"{prefix}Level {i}{level.HashValidity.GetValidityString()}:"); + PrintItem(sb, colLen, $"{prefix2}Data Offset:", $"0x{level.LogicalOffset.ToString($"x{offsetLen}")}"); + PrintItem(sb, colLen, $"{prefix2}Data Size:", $"0x{level.HashDataSize.ToString($"x{offsetLen}")}"); + PrintItem(sb, colLen, $"{prefix2}Hash Offset:", $"0x{hashOffset.ToString($"x{offsetLen}")}"); + PrintItem(sb, colLen, $"{prefix2}Hash BlockSize:", $"0x{1 << level.BlockSizePower:x8}"); + } + } } } diff --git a/hactoolnet/ProcessNca.cs b/hactoolnet/ProcessNca.cs index 1b87a1e8..758d9ad7 100644 --- a/hactoolnet/ProcessNca.cs +++ b/hactoolnet/ProcessNca.cs @@ -174,7 +174,7 @@ namespace hactoolnet PrintSha256Hash(sect); break; case NcaHashType.Ivfc: - PrintIvfcHash(sect); + PrintIvfcHash(sb, colLen, 8, sect.Header.IvfcInfo, IntegrityStreamType.RomFs); break; default: sb.AppendLine(" Unknown/invalid superblock!"); @@ -196,32 +196,6 @@ namespace hactoolnet PrintItem(sb, colLen, " PFS0 Offset:", $"0x{hashInfo.DataOffset:x12}"); PrintItem(sb, colLen, " PFS0 Size:", $"0x{hashInfo.DataSize:x12}"); } - - void PrintIvfcHash(NcaSection sect) - { - IvfcHeader ivfcInfo = sect.Header.IvfcInfo; - - PrintItem(sb, colLen, $" Master Hash{sect.MasterHashValidity.GetValidityString()}:", ivfcInfo.MasterHash); - PrintItem(sb, colLen, " Magic:", ivfcInfo.Magic); - PrintItem(sb, colLen, " Version:", $"{ivfcInfo.Version:x8}"); - - for (int i = 0; i < Romfs.IvfcMaxLevel; i++) - { - IvfcLevelHeader level = ivfcInfo.LevelHeaders[i]; - long hashOffset = 0; - - if (i != 0) - { - hashOffset = ivfcInfo.LevelHeaders[i - 1].LogicalOffset; - } - - sb.AppendLine($" Level {i}{level.HashValidity.GetValidityString()}:"); - PrintItem(sb, colLen, " Data Offset:", $"0x{level.LogicalOffset:x12}"); - PrintItem(sb, colLen, " Data Size:", $"0x{level.HashDataSize:x12}"); - PrintItem(sb, colLen, " Hash Offset:", $"0x{hashOffset:x12}"); - PrintItem(sb, colLen, " Hash BlockSize:", $"0x{1 << level.BlockSizePower:x8}"); - } - } } } } diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index 3d5743ce..dce56572 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Text; using LibHac; using LibHac.Save; @@ -16,6 +15,11 @@ namespace hactoolnet { var save = new Savefile(ctx.Keyset, file, ctx.Options.IntegrityLevel); + if (ctx.Options.Validate) + { + save.Verify(ctx.Logger); + } + if (ctx.Options.OutDir != null) { save.Extract(ctx.Options.OutDir, ctx.Logger); @@ -79,6 +83,14 @@ namespace hactoolnet } } + if (ctx.Options.ListFiles) + { + foreach (FileEntry fileEntry in save.Files) + { + ctx.Logger.LogMessage(fileEntry.FullPath); + } + } + ctx.Logger.LogMessage(save.Print()); } } @@ -100,17 +112,9 @@ namespace hactoolnet PrintItem(sb, colLen, "Save Data Size:", $"0x{save.Header.ExtraData.DataSize:x16} ({Util.GetBytesReadable(save.Header.ExtraData.DataSize)})"); PrintItem(sb, colLen, "Journal Size:", $"0x{save.Header.ExtraData.JournalSize:x16} ({Util.GetBytesReadable(save.Header.ExtraData.JournalSize)})"); PrintItem(sb, colLen, $"Header Hash{save.Header.HeaderHashValidity.GetValidityString()}:", save.Header.Layout.Hash); - PrintItem(sb, colLen, "IVFC Salt Seed:", save.Header.Ivfc.SaltSource); PrintItem(sb, colLen, "Number of Files:", save.Files.Length); - if (save.Files.Length > 0 && save.Files.Length < 100) - { - sb.AppendLine("Files:"); - foreach (FileEntry file in save.Files.OrderBy(x => x.FullPath)) - { - sb.AppendLine(file.FullPath); - } - } + PrintIvfcHash(sb, colLen, 4, save.Header.Ivfc, IntegrityStreamType.Save); return sb.ToString(); } From 7a7946a26c584df9b056b8edd2bb9b22138d4219 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 18 Oct 2018 18:18:39 -0500 Subject: [PATCH 10/13] Add Savefile.CommitHeader method --- LibHac/Crypto.cs | 8 ++++++ LibHac/Save/Header.cs | 9 ++++++- LibHac/Save/Savefile.cs | 55 ++++++++++++++++++++++----------------- hactoolnet/ProcessSave.cs | 2 +- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/LibHac/Crypto.cs b/LibHac/Crypto.cs index 6c8551a8..db1f0932 100644 --- a/LibHac/Crypto.cs +++ b/LibHac/Crypto.cs @@ -21,6 +21,14 @@ namespace LibHac return comp; } + public static byte[] ComputeSha256(byte[] data, int offset, int count) + { + using (SHA256 sha = SHA256.Create()) + { + return sha.ComputeHash(data, offset, count); + } + } + public static void DecryptEcb(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length) { using (Aes aes = Aes.Create()) diff --git a/LibHac/Save/Header.cs b/LibHac/Save/Header.cs index 17afa2f8..70da2619 100644 --- a/LibHac/Save/Header.cs +++ b/LibHac/Save/Header.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using LibHac.Streams; namespace LibHac.Save { @@ -24,13 +25,17 @@ namespace LibHac.Save public byte[] DuplexMasterA { get; } public byte[] DuplexMasterB { get; } + public Stream MasterHash { get; } + public Validity SignatureValidity { get; } public Validity HeaderHashValidity { get; } public byte[] Data { get; } - public Header(Keyset keyset, BinaryReader reader) + public Header(Keyset keyset, SharedStreamSource streamSource) { + var reader = new BinaryReader(streamSource.CreateStream()); + reader.BaseStream.Position = 0; Data = reader.ReadBytes(0x4000); reader.BaseStream.Position = 0; @@ -65,6 +70,8 @@ namespace LibHac.Save reader.BaseStream.Position = Layout.IvfcMasterHashOffsetB; MasterHashB = reader.ReadBytes((int)Layout.IvfcMasterHashSize); + MasterHash = streamSource.CreateStream(Layout.IvfcMasterHashOffsetA, Layout.IvfcMasterHashSize); + reader.BaseStream.Position = Layout.DuplexMasterOffsetA; DuplexMasterA = reader.ReadBytes((int)Layout.DuplexMasterSize); reader.BaseStream.Position = Layout.DuplexMasterOffsetB; diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index 3ab2f999..172bfe49 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -28,33 +28,30 @@ namespace LibHac.Save { SavefileSource = new SharedStreamSource(file); - using (var reader = new BinaryReader(SavefileSource.CreateStream(), Encoding.Default, true)) - { - Header = new Header(keyset, reader); - FsLayout layout = Header.Layout; + Header = new Header(keyset, SavefileSource); + FsLayout layout = Header.Layout; - DataRemapStorage = new RemapStorage(SavefileSource.CreateStream(layout.FileMapDataOffset, layout.FileMapDataSize), - Header.FileRemap, Header.FileMapEntries); + DataRemapStorage = new RemapStorage(SavefileSource.CreateStream(layout.FileMapDataOffset, layout.FileMapDataSize), + Header.FileRemap, Header.FileMapEntries); - DuplexData = InitDuplexStream(DataRemapStorage, Header); + DuplexData = InitDuplexStream(DataRemapStorage, Header); - MetaRemapStorage = new RemapStorage(DuplexData, Header.MetaRemap, Header.MetaMapEntries); + MetaRemapStorage = new RemapStorage(DuplexData, Header.MetaRemap, Header.MetaMapEntries); - Stream journalTable = MetaRemapStorage.OpenStream(layout.JournalTableOffset, layout.JournalTableSize); + Stream journalTable = MetaRemapStorage.OpenStream(layout.JournalTableOffset, layout.JournalTableSize); - MappingEntry[] journalMap = JournalStream.ReadMappingEntries(journalTable, Header.Journal.MainDataBlockCount); + MappingEntry[] journalMap = JournalStream.ReadMappingEntries(journalTable, Header.Journal.MainDataBlockCount); - Stream journalData = DataRemapStorage.OpenStream(layout.JournalDataOffset, - layout.JournalDataSizeB + layout.SizeReservedArea); - var journalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); - JournalStreamSource = new SharedStreamSource(journalStream); + Stream journalData = DataRemapStorage.OpenStream(layout.JournalDataOffset, + layout.JournalDataSizeB + layout.SizeReservedArea); + var journalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); + JournalStreamSource = new SharedStreamSource(journalStream); - IvfcStream = InitIvfcStream(integrityCheckLevel); + IvfcStream = InitIvfcStream(integrityCheckLevel); - SaveFs = new SaveFs(IvfcStream, MetaRemapStorage.OpenStream(layout.FatOffset, layout.FatSize), Header.Save); + SaveFs = new SaveFs(IvfcStream, MetaRemapStorage.OpenStream(layout.FatOffset, layout.FatSize), Header.Save); - IvfcStreamSource = new SharedStreamSource(IvfcStream); - } + IvfcStreamSource = new SharedStreamSource(IvfcStream); } private static LayeredDuplexFs InitDuplexStream(RemapStorage baseStorage, Header header) @@ -95,7 +92,7 @@ namespace LibHac.Save initInfo[0] = new IntegrityVerificationInfo { - Data = new MemoryStream(Header.MasterHashA), + Data = Header.MasterHash, BlockSize = 0, Type = IntegrityStreamType.Save }; @@ -132,18 +129,28 @@ namespace LibHac.Save public bool FileExists(string filename) => SaveFs.FileExists(filename); - public bool SignHeader(Keyset keyset) + public bool CommitHeader(Keyset keyset) { + SharedStream headerStream = SavefileSource.CreateStream(); + + var hashData = new byte[0x3d00]; + + headerStream.Position = 0x300; + headerStream.Read(hashData, 0, hashData.Length); + + byte[] hash = Crypto.ComputeSha256(hashData, 0, hashData.Length); + headerStream.Position = 0x108; + headerStream.Write(hash, 0, hash.Length); + if (keyset.SaveMacKey.IsEmpty()) return false; - var data = new byte[0x200]; + var cmacData = new byte[0x200]; var cmac = new byte[0x10]; - SharedStream headerStream = SavefileSource.CreateStream(); headerStream.Position = 0x100; - headerStream.Read(data, 0, 0x200); + headerStream.Read(cmacData, 0, 0x200); - Crypto.CalculateAesCmac(keyset.SaveMacKey, data, 0, cmac, 0, 0x200); + Crypto.CalculateAesCmac(keyset.SaveMacKey, cmacData, 0, cmac, 0, 0x200); headerStream.Position = 0; headerStream.Write(cmac, 0, 0x10); diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index dce56572..3fd3f551 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -73,7 +73,7 @@ namespace hactoolnet if (ctx.Options.SignSave) { - if (save.SignHeader(ctx.Keyset)) + if (save.CommitHeader(ctx.Keyset)) { ctx.Logger.LogMessage("Successfully signed save file"); } From e9f75566356000b9a73c935f15f9b6a520c44944 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 22 Oct 2018 12:51:30 -0500 Subject: [PATCH 11/13] Ensure the save header is flushed when committing --- LibHac/Save/Savefile.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LibHac/Save/Savefile.cs b/LibHac/Save/Savefile.cs index 172bfe49..9c305b97 100644 --- a/LibHac/Save/Savefile.cs +++ b/LibHac/Save/Savefile.cs @@ -154,6 +154,7 @@ namespace LibHac.Save headerStream.Position = 0; headerStream.Write(cmac, 0, 0x10); + headerStream.Flush(); return true; } From 0abd48cfb5c24298b29760ea4a8ce4b36ab8f696 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 22 Oct 2018 17:59:02 -0500 Subject: [PATCH 12/13] Print key info in MissingKeyException message --- LibHac/Keyset.cs | 1 + LibHac/MissingKeyException.cs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/LibHac/Keyset.cs b/LibHac/Keyset.cs index 9f25203f..2d68d367 100644 --- a/LibHac/Keyset.cs +++ b/LibHac/Keyset.cs @@ -539,6 +539,7 @@ namespace LibHac public enum KeyType { + None, Common, Unique, Title diff --git a/LibHac/MissingKeyException.cs b/LibHac/MissingKeyException.cs index 8d1e1b4d..f9272dc9 100644 --- a/LibHac/MissingKeyException.cs +++ b/LibHac/MissingKeyException.cs @@ -75,5 +75,25 @@ namespace LibHac base.GetObjectData(info, context); info.AddValue(nameof(Name), Name); } + + public override string Message + { + get + { + string s = base.Message; + + if (Type != KeyType.None) + { + s += $"{Environment.NewLine}Key Type: {Type}"; + } + + if (Name != null) + { + s += $"{Environment.NewLine}Key Name: {Name}"; + } + + return s; + } + } } } From 92d0a2e6c2dca7cba800e10de8bfde24e4c1d2ad Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 22 Oct 2018 18:00:52 -0500 Subject: [PATCH 13/13] Version 0.1.3 --- LibHac/LibHac.csproj | 2 +- hactoolnet/hactoolnet.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LibHac/LibHac.csproj b/LibHac/LibHac.csproj index 59228d90..b957b21d 100644 --- a/LibHac/LibHac.csproj +++ b/LibHac/LibHac.csproj @@ -16,7 +16,7 @@ git https://github.com/Thealexbarney/LibHac - 0.1.2 + 0.1.3 $(MSBuildProjectDirectory)=C:/LibHac/ true true diff --git a/hactoolnet/hactoolnet.csproj b/hactoolnet/hactoolnet.csproj index 3fe08ca1..ef3e5212 100644 --- a/hactoolnet/hactoolnet.csproj +++ b/hactoolnet/hactoolnet.csproj @@ -7,7 +7,7 @@ - 0.1.2 + 0.1.3 $(MSBuildProjectDirectory)=C:/hactoolnet/