mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
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:
parent
8d0bd79708
commit
e217979000
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user