Misc zipreader tweaks

Closes #4566
signature changes, add some overloads, extract/simplify common logic

Co-Authored-By: Chris Dailey <nitz@users.noreply.github.com>
This commit is contained in:
Kurt 2025-09-25 17:27:14 -05:00
parent 8d0bd79708
commit e217979000
4 changed files with 125 additions and 32 deletions

View File

@ -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 <see cref="data"/>
/// </summary>
/// <param name="data">Raw input data</param>
/// <param name="result">The resulting <see cref="SaveFile"/> if successful, otherwise null.</param>
/// <param name="path">Optional file path.</param>
/// <returns>Save File object, or null if invalid. Check <see cref="ISaveHandler"/> if it is compatible first.</returns>
SaveFile? ReadSaveFile(Memory<byte> data, string? path = null);
bool TryRead(Memory<byte> data, [NotNullWhen(true)] out SaveFile? result, string? path = null);
/// <inheritdoc cref="ISaveHandler.IsRecognized"/>
bool IsRecognized(long dataLength);

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
@ -9,64 +10,155 @@ namespace PKHeX.Core;
/// </summary>
public sealed class ZipReader : ISaveReader
{
/// <summary>
/// Indicates if the provided data length is large enough to attempt ZIP recognition.
/// </summary>
/// <param name="dataLength">Length of the data buffer.</param>
/// <returns><see langword="true"/> if the data length is large enough; otherwise, <see langword="false"/>.</returns>
public bool IsRecognized(long dataLength) => dataLength > 4;
private static bool IsValidFileName(string name) => name is "main" or "SaveData.bin";
private static bool IsValidFileName(ReadOnlySpan<char> name) => Is(name, "main") || Is(name, "SaveData.bin");
private static bool Is(ReadOnlySpan<char> value, ReadOnlySpan<char> other) => value.Equals(other, StringComparison.OrdinalIgnoreCase);
public SaveFile? ReadSaveFile(Memory<byte> data, string? path = null)
// check ZIP header in first 4 bytes
private static bool IsPossiblyZip(ReadOnlySpan<byte> data) => data.Length >= 16 && data is [0x50, 0x4B, 0x03, 0x04, ..]; // "PK\x03\x04"
/// <summary>
/// Attempts to read a <see cref="SaveFile"/> from the provided ZIP data.
/// </summary>
/// <param name="data">Raw file data that may represent a ZIP archive.</param>
/// <param name="result">When this method returns <see langword="true"/>, contains the parsed <see cref="SaveFile"/> instance; otherwise <see langword="null"/>.</param>
/// <param name="path">Optional original file path (ignored for ZIP contents).</param>
/// <returns><see langword="true"/> if a save file was successfully parsed; otherwise, <see langword="false"/>.</returns>
// ReSharper disable once MethodOverloadWithOptionalParameter
public bool TryRead(Memory<byte> data, [NotNullWhen(true)] out SaveFile? result, string? path = null) => TryRead(data, out result);
/// <summary>
/// Attempts to read a <see cref="SaveFile"/> from the provided ZIP data.
/// </summary>
/// <param name="data">Raw file data that may represent a ZIP archive.</param>
/// <param name="result">When this method returns <see langword="true"/>, contains the parsed <see cref="SaveFile"/> instance; otherwise <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a save file was successfully parsed; otherwise, <see langword="false"/>.</returns>
public static bool TryRead(Memory<byte> 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)
/// <inheritdoc cref="TryRead(Memory{byte}, out SaveFile?)"/>
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);
}
/// <summary>
/// Attempts to read a <see cref="SaveFile"/> from the provided stream assumed to contain a ZIP archive.
/// </summary>
/// <param name="ms">Stream positioned at the beginning of a ZIP archive.</param>
/// <param name="result">When this method returns <see langword="true"/>, contains the parsed <see cref="SaveFile"/> instance; otherwise <see langword="null"/>.</param>
/// <param name="leaveOpen">Whether to leave the stream open after reading.</param>
/// <returns><see langword="true"/> if a save file was successfully parsed; otherwise, <see langword="false"/>.</returns>
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<byte> 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);
}
/// <summary>
/// Updates the first valid save file entry in a ZIP file on disk with the provided save data.
/// </summary>
/// <param name="path">Path to an existing ZIP file.</param>
/// <param name="data">New save data to write.</param>
/// <returns><see langword="true"/> if an entry was updated; otherwise, <see langword="false"/>.</returns>
public static bool Update(string path, ReadOnlySpan<byte> 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<byte> data)
/// <summary>
/// Updates the first save entry in the provided <see cref="ZipArchive"/> with the given data.
/// </summary>
/// <param name="zip">Open ZIP archive in update mode.</param>
/// <param name="data">New save data to write.</param>
/// <returns><see langword="true"/> if an entry was updated; otherwise, <see langword="false"/>.</returns>
public static bool Update(ZipArchive zip, ReadOnlySpan<byte> 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<byte> data)
{
using var entryStream = entry.Open();
entryStream.SetLength(0);
entryStream.Write(data);
}
}

View File

@ -505,8 +505,7 @@ private static bool TryGetSaveFileCustom(Memory<byte> 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;

View File

@ -435,7 +435,7 @@ private static void ExportSAVInternal(ReadOnlySpan<byte> data, string path, stri
if (path != exist)
File.Copy(exist, path, true);
ZipReader.UpdateSaveFile(data, path);
ZipReader.Update(path, data);
return;
}
}