diff --git a/src/LibHac/IO/Save/AllocationTable.cs b/src/LibHac/IO/Save/AllocationTable.cs index e560b6c7..a8bc04f2 100644 --- a/src/LibHac/IO/Save/AllocationTable.cs +++ b/src/LibHac/IO/Save/AllocationTable.cs @@ -299,6 +299,42 @@ namespace LibHac.IO.Save return totalLength; } + public void FsTrimList(int blockIndex) + { + int index = blockIndex; + + int tableSize = Header.AllocationTableBlockCount; + int nodesIterated = 0; + + while (index != -1) + { + ReadEntry(index, out int next, out int _, out int length); + + if (length > 3) + { + int fillOffset = BlockToEntryIndex(index + 2) * EntrySize; + int fillLength = (length - 3) * EntrySize; + + BaseStorage.Slice(fillOffset, fillLength).Fill(0x00); + } + + nodesIterated++; + + if (nodesIterated > tableSize) + { + return; + } + + index = next; + } + } + + public void FsTrim() + { + int tableSize = BlockToEntryIndex(Header.AllocationTableBlockCount) * EntrySize; + BaseStorage.Slice(tableSize).Fill(0x00); + } + private void ReadEntries(int entryIndex, Span entries) { Debug.Assert(entries.Length >= 2); diff --git a/src/LibHac/IO/Save/AllocationTableStorage.cs b/src/LibHac/IO/Save/AllocationTableStorage.cs index 492749ad..44b4673c 100644 --- a/src/LibHac/IO/Save/AllocationTableStorage.cs +++ b/src/LibHac/IO/Save/AllocationTableStorage.cs @@ -11,13 +11,14 @@ namespace LibHac.IO.Save private long _length; - public AllocationTableStorage(IStorage data, AllocationTable table, int blockSize, int initialBlock, long length) + public AllocationTableStorage(IStorage data, AllocationTable table, int blockSize, int initialBlock) { BaseStorage = data; BlockSize = blockSize; - _length = length; Fat = table; InitialBlock = initialBlock; + + _length = table.GetListLength(initialBlock) * blockSize; } protected override void ReadImpl(Span destination, long offset) diff --git a/src/LibHac/IO/Save/SaveDataFileSystem.cs b/src/LibHac/IO/Save/SaveDataFileSystem.cs index 86312cb2..aa8c2ff1 100644 --- a/src/LibHac/IO/Save/SaveDataFileSystem.cs +++ b/src/LibHac/IO/Save/SaveDataFileSystem.cs @@ -200,6 +200,11 @@ namespace LibHac.IO.Save return true; } + public void FsTrim() + { + SaveDataFileSystemCore.FsTrim(); + } + public Validity Verify(IProgressReport logger = null) { Validity validity = IvfcStorage.Validate(true, logger); diff --git a/src/LibHac/IO/Save/SaveDataFileSystemCore.cs b/src/LibHac/IO/Save/SaveDataFileSystemCore.cs index 12966a8f..d1bd0f27 100644 --- a/src/LibHac/IO/Save/SaveDataFileSystemCore.cs +++ b/src/LibHac/IO/Save/SaveDataFileSystemCore.cs @@ -20,9 +20,8 @@ namespace LibHac.IO.Save Header = new SaveHeader(HeaderStorage); - // todo: Query the FAT for the file size when none is given - AllocationTableStorage dirTableStorage = OpenFatBlock(AllocationTable.Header.DirectoryTableBlock, 1000000); - AllocationTableStorage fileTableStorage = OpenFatBlock(AllocationTable.Header.FileTableBlock, 1000000); + AllocationTableStorage dirTableStorage = OpenFatStorage(AllocationTable.Header.DirectoryTableBlock); + AllocationTableStorage fileTableStorage = OpenFatStorage(AllocationTable.Header.FileTableBlock); FileTable = new HierarchicalSaveFileTable(dirTableStorage, fileTableStorage); } @@ -92,7 +91,7 @@ namespace LibHac.IO.Save return new NullFile(); } - AllocationTableStorage storage = OpenFatBlock(file.StartBlock, file.Length); + AllocationTableStorage storage = OpenFatStorage(file.StartBlock); return new SaveDataFile(storage, 0, file.Length, mode); } @@ -139,9 +138,31 @@ namespace LibHac.IO.Save public IStorage GetBaseStorage() => BaseStorage.AsReadOnly(); public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly(); - private AllocationTableStorage OpenFatBlock(int blockIndex, long size) + public void FsTrim() { - return new AllocationTableStorage(BaseStorage, AllocationTable, (int)Header.BlockSize, blockIndex, size); + AllocationTable.FsTrim(); + + foreach (DirectoryEntry file in this.EnumerateEntries("*", SearchOptions.RecurseSubdirectories)) + { + if (FileTable.TryOpenFile(file.FullPath, out SaveFileInfo fileInfo) && fileInfo.StartBlock >= 0) + { + AllocationTable.FsTrimList(fileInfo.StartBlock); + + OpenFatStorage(fileInfo.StartBlock).Slice(fileInfo.Length).Fill(0); + } + } + + int freeIndex = AllocationTable.GetFreeListBlockIndex(); + if (freeIndex == 0) return; + + AllocationTable.FsTrimList(freeIndex); + + OpenFatStorage(freeIndex).Fill(0); + } + + private AllocationTableStorage OpenFatStorage(int blockIndex) + { + return new AllocationTableStorage(BaseStorage, AllocationTable, (int)Header.BlockSize, blockIndex); } } diff --git a/src/LibHac/IO/StorageExtensions.cs b/src/LibHac/IO/StorageExtensions.cs index e71c1f4f..9be88414 100644 --- a/src/LibHac/IO/StorageExtensions.cs +++ b/src/LibHac/IO/StorageExtensions.cs @@ -97,6 +97,59 @@ namespace LibHac.IO progress?.SetTotal(0); } + public static void Fill(this IStorage input, byte value, IProgressReport progress = null) + { + const int threshold = 0x400; + + long length = input.GetSize(); + if (length > threshold) + { + input.FillLarge(value, progress); + return; + } + + Span buf = stackalloc byte[(int)length]; + buf.Fill(value); + + input.Write(buf, 0); + } + + private static void FillLarge(this IStorage input, byte value, IProgressReport progress = null) + { + const int bufferSize = 0x4000; + + long remaining = input.GetSize(); + if (remaining < 0) throw new ArgumentException("Storage must have an explicit length"); + progress?.SetTotal(remaining); + + long pos = 0; + + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + buffer.AsSpan(0, (int)Math.Min(remaining, bufferSize)).Fill(value); + + while (remaining > 0) + { + int toFill = (int)Math.Min(bufferSize, remaining); + Span buf = buffer.AsSpan(0, toFill); + + input.Write(buf, pos); + + remaining -= toFill; + pos += toFill; + + progress?.ReportAdd(toFill); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + progress?.SetTotal(0); + } + public static void WriteAllBytes(this IStorage input, string filename, IProgressReport progress = null) { using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.Write)) diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs index df3c1749..52c090cb 100644 --- a/src/hactoolnet/CliParser.cs +++ b/src/hactoolnet/CliParser.cs @@ -52,6 +52,7 @@ namespace hactoolnet 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("trim", 0, (o, a) => o.TrimSave = true), new CliOption("readbench", 0, (o, a) => o.ReadBench = true), new CliOption("hashedfs", 0, (o, a) => o.BuildHfs = true), new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), @@ -232,6 +233,7 @@ 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(" --trim Trim garbage data in the save file. (Requires device_key in key file)"); sb.AppendLine(" --listfiles List files in save file."); sb.AppendLine(" --replacefile Replaces a file in the save data"); sb.AppendLine("NDV0 (Delta) options:"); diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index f4fc1038..c99db2c4 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -45,6 +45,7 @@ namespace hactoolnet public bool ListRomFs; public bool ListFiles; public bool SignSave; + public bool TrimSave; public bool ReadBench; public bool BuildHfs; public ulong TitleId; diff --git a/src/hactoolnet/ProcessSave.cs b/src/hactoolnet/ProcessSave.cs index d8b397a6..f7626a5a 100644 --- a/src/hactoolnet/ProcessSave.cs +++ b/src/hactoolnet/ProcessSave.cs @@ -129,8 +129,14 @@ namespace hactoolnet return; } - if (ctx.Options.SignSave) + if (ctx.Options.SignSave || ctx.Options.TrimSave) { + if (ctx.Options.TrimSave) + { + save.FsTrim(); + ctx.Logger.LogMessage("Trimmed save file"); + } + if (save.Commit(ctx.Keyset)) { ctx.Logger.LogMessage("Successfully signed save file");