diff --git a/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs index d1a76f9f2..99fbacd8b 100644 --- a/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs +++ b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace PKHeX.Core; #if !(EXCLUDE_EMULATOR_FORMATS && EXCLUDE_HACKS) @@ -39,9 +40,10 @@ public interface ISaveReader /// Reads a save file from the /// /// Raw input data + /// The resulting if successful, otherwise null. /// Optional file path. /// Save File object, or null if invalid. Check if it is compatible first. - SaveFile? ReadSaveFile(Memory data, string? path = null); + bool TryRead(Memory data, [NotNullWhen(true)] out SaveFile? result, string? path = null); /// bool IsRecognized(long dataLength); diff --git a/PKHeX.Core/Saves/Util/Recognition/ZipReader.cs b/PKHeX.Core/Saves/Util/Recognition/ZipReader.cs index 956b1c1c1..5db1afb01 100644 --- a/PKHeX.Core/Saves/Util/Recognition/ZipReader.cs +++ b/PKHeX.Core/Saves/Util/Recognition/ZipReader.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; @@ -9,64 +10,155 @@ namespace PKHeX.Core; /// public sealed class ZipReader : ISaveReader { + /// + /// Indicates if the provided data length is large enough to attempt ZIP recognition. + /// + /// Length of the data buffer. + /// if the data length is large enough; otherwise, . public bool IsRecognized(long dataLength) => dataLength > 4; - private static bool IsValidFileName(string name) => name is "main" or "SaveData.bin"; + private static bool IsValidFileName(ReadOnlySpan name) => Is(name, "main") || Is(name, "SaveData.bin"); + private static bool Is(ReadOnlySpan value, ReadOnlySpan other) => value.Equals(other, StringComparison.OrdinalIgnoreCase); - public SaveFile? ReadSaveFile(Memory data, string? path = null) + // check ZIP header in first 4 bytes + private static bool IsPossiblyZip(ReadOnlySpan data) => data.Length >= 16 && data is [0x50, 0x4B, 0x03, 0x04, ..]; // "PK\x03\x04" + + /// + /// Attempts to read a from the provided ZIP data. + /// + /// Raw file data that may represent a ZIP archive. + /// When this method returns , contains the parsed instance; otherwise . + /// Optional original file path (ignored for ZIP contents). + /// if a save file was successfully parsed; otherwise, . + // ReSharper disable once MethodOverloadWithOptionalParameter + public bool TryRead(Memory data, [NotNullWhen(true)] out SaveFile? result, string? path = null) => TryRead(data, out result); + + /// + /// Attempts to read a from the provided ZIP data. + /// + /// Raw file data that may represent a ZIP archive. + /// When this method returns , contains the parsed instance; otherwise . + /// if a save file was successfully parsed; otherwise, . + public static bool TryRead(Memory data, [NotNullWhen(true)] out SaveFile? result) { - // check ZIP header in first 4 bytes - if (data.Length < 4 || data.Span is not [0x50, 0x4B, 0x03, 0x04, ..]) // "PK\x03\x04" - return null; + result = null; + if (!IsPossiblyZip(data.Span)) + return false; using var ms = new MemoryStream(data.ToArray()); - using var archive = new ZipArchive(ms, ZipArchiveMode.Read, false); - return Read(archive); + return TryRead(ms, out result); } - private static SaveFile? Read(ZipArchive zip) + /// + public static bool TryRead(byte[] data, [NotNullWhen(true)] out SaveFile? result) + { + result = null; + if (!IsPossiblyZip(data)) + return false; + + using var ms = new MemoryStream(data); + return TryRead(ms, out result); + } + + /// + /// Attempts to read a from the provided stream assumed to contain a ZIP archive. + /// + /// Stream positioned at the beginning of a ZIP archive. + /// When this method returns , contains the parsed instance; otherwise . + /// Whether to leave the stream open after reading. + /// if a save file was successfully parsed; otherwise, . + public static bool TryRead(Stream ms, [NotNullWhen(true)] out SaveFile? result, bool leaveOpen = false) + { + try + { + using var archive = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen); + return TryRead(archive, out result); + } + catch + { + result = null; + return false; + } + } + + private static bool TryRead(ZipArchive zip, [NotNullWhen(true)] out SaveFile? result) { var entries = zip.Entries; + if (entries.Count == 1) + return TryRead(entries[0], out result); + foreach (var entry in entries) { - if (!IsValidFileName(entry.Name) && entries.Count != 1) + if (!IsValidFileName(entry.Name)) continue; - if (!SaveUtil.IsSizeValid(entry.Length)) - continue; - - using var entryStream = entry.Open(); - var tmp = new MemoryStream(); - entryStream.CopyTo(tmp); - if (!SaveUtil.TryGetSaveFile(tmp.ToArray(), out var result, entry.FullName)) - continue; - return result; + if (TryRead(entry, out result)) + return true; } - return null; + result = null; + return false; } - public static void UpdateSaveFile(ReadOnlySpan data, string path) + private static bool TryRead(ZipArchiveEntry entry, [NotNullWhen(true)] out SaveFile? result) + { + if (!SaveUtil.IsSizeValid(entry.Length)) + { + result = null; + return false; + } + + using var entryStream = entry.Open(); + var tmp = new MemoryStream(); + entryStream.CopyTo(tmp); + return SaveUtil.TryGetSaveFile(tmp.ToArray(), out result, entry.FullName); + } + + /// + /// Updates the first valid save file entry in a ZIP file on disk with the provided save data. + /// + /// Path to an existing ZIP file. + /// New save data to write. + /// if an entry was updated; otherwise, . + public static bool Update(string path, ReadOnlySpan data) { var ext = Path.GetExtension(path); if (ext is not ".zip") - return; + return false; if (!File.Exists(path)) - return; + return false; using var zip = ZipFile.Open(path, ZipArchiveMode.Update); - Update(zip, data); + return Update(zip, data); } - private static void Update(ZipArchive zip, ReadOnlySpan data) + /// + /// Updates the first save entry in the provided with the given data. + /// + /// Open ZIP archive in update mode. + /// New save data to write. + /// if an entry was updated; otherwise, . + public static bool Update(ZipArchive zip, ReadOnlySpan data) { var entries = zip.Entries; + if (entries.Count == 1) + { + UpdateEntry(entries[0], data); + return true; + } foreach (var entry in entries) { - if (!IsValidFileName(entry.Name) && entries.Count != 1) + if (!IsValidFileName(entry.Name)) continue; - using var entryStream = entry.Open(); - entryStream.Write(data); - break; + UpdateEntry(entry, data); + return true; } + return false; + } + + private static void UpdateEntry(ZipArchiveEntry entry, ReadOnlySpan data) + { + using var entryStream = entry.Open(); + entryStream.SetLength(0); + entryStream.Write(data); } } diff --git a/PKHeX.Core/Saves/Util/SaveUtil.cs b/PKHeX.Core/Saves/Util/SaveUtil.cs index 44793373f..91c3840f4 100644 --- a/PKHeX.Core/Saves/Util/SaveUtil.cs +++ b/PKHeX.Core/Saves/Util/SaveUtil.cs @@ -505,8 +505,7 @@ private static bool TryGetSaveFileCustom(Memory data, [NotNullWhen(true)] if (!h.IsRecognized(data.Length)) continue; - result = h.ReadSaveFile(data, path); - if (result is not null) + if (h.TryRead(data, out result, path)) return true; } result = null; diff --git a/PKHeX.WinForms/Util/WinFormsUtil.cs b/PKHeX.WinForms/Util/WinFormsUtil.cs index 22050f60a..f470a72e3 100644 --- a/PKHeX.WinForms/Util/WinFormsUtil.cs +++ b/PKHeX.WinForms/Util/WinFormsUtil.cs @@ -435,7 +435,7 @@ private static void ExportSAVInternal(ReadOnlySpan data, string path, stri if (path != exist) File.Copy(exist, path, true); - ZipReader.UpdateSaveFile(data, path); + ZipReader.Update(path, data); return; } }