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;
}
}