using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; /// /// Logic for detecting supported binary object formats. /// public static class FileUtil { /// /// Attempts to get a binary object from the provided path. /// /// Path to the file. /// Reference SaveFile used for PC Binary compatibility checks. /// Supported file object reference, null if none found. public static object? GetSupportedFile(string path, SaveFile? reference = null) { try { var fi = new FileInfo(path); if (!fi.Exists || IsFileTooBig(fi.Length) || IsFileTooSmall(fi.Length)) return null; var data = File.ReadAllBytes(path); var ext = Path.GetExtension(path.AsSpan()); return GetSupportedFile(data, ext, reference); } // User input data can be fuzzed; if anything blows up, just fail safely. catch (Exception e) { Debug.WriteLine(MessageStrings.MsgFileInUse); Debug.WriteLine(e.Message); return null; } } /// /// Attempts to get a binary object from the provided inputs. /// /// Binary data for the file. /// File extension used as a hint. /// Reference SaveFile used for PC Binary compatibility checks. /// Supported file object reference, null if none found. public static object? GetSupportedFile(Memory data, ReadOnlySpan ext, SaveFile? reference = null) { if (SaveUtil.TryGetSaveFile(data, out var sav)) return sav; if (TryGetMemoryCard(data, out var mc)) return mc; if (TryGetPKM(data, out var pk, ext)) return pk; if (TryGetPCBoxBin(data, out var concat, reference)) return concat; if (TryGetBattleVideo(data, out var bv)) return bv; if (TryGetMysteryGift(data, out var g, ext)) return g; if (TryGetGP1(data, out var gp)) return gp; if (TryGetBundle(data, out var bundle)) return bundle; return null; } public static bool IsFileLocked(string path) { try { return (File.GetAttributes(path) & FileAttributes.ReadOnly) != 0; } catch { return true; } } public static long GetFileSize(string path) { try { var fi = new FileInfo(path); var size = fi.Length; if (size > int.MaxValue) return -1; return size; } catch { return -1; } // Bad File / Locked } /// /// Safely iterates over the elements of the specified , handling exceptions during enumeration. /// /// /// This method ensures that exceptions thrown during enumeration do not terminate the iteration prematurely. /// Instead, it logs the exception (if a action is provided) and continues iterating until the specified limit is reached. /// If the limit is exceeded, the iteration stops. /// /// The type of elements in the source collection. /// The source collection to iterate over. Cannot be . /// /// The maximum number of consecutive exceptions allowed before the iteration is terminated. /// Must be greater than or equal to 0. /// /// An optional action to log or handle exceptions that occur during enumeration. If , exceptions are ignored. /// An that yields elements from the source collection, skipping over elements that cause exceptions. public static IEnumerable IterateSafe(this IEnumerable source, int failOut = 10, Action? log = null) { using var enumerator = source.GetEnumerator(); int ctr = 0; while (true) { try { var next = enumerator.MoveNext(); if (!next) yield break; } catch (Exception ex) { log?.Invoke(ex); if (++ctr >= failOut) yield break; continue; } ctr = 0; yield return enumerator.Current; } } private static bool TryGetGP1(Memory data, [NotNullWhen(true)] out GP1? gp1) { gp1 = null; if (data.Length != GP1.SIZE || ReadUInt32LittleEndian(data.Span[0x28..]) == 0) return false; gp1 = new GP1(data); return true; } private static bool TryGetBundle(Memory data, [NotNullWhen(true)] out IPokeGroup? result) { result = null; if (RentalTeam8.IsRentalTeam(data)) { result = new RentalTeam8(data); return true; } if (RentalTeam9.IsRentalTeam(data)) { result = new RentalTeam9(data); return true; } if (RentalTeamSet9.IsRentalTeamSet(data)) { result = new RentalTeamSet9(data); return true; } return false; } /// /// Checks if the length is too big to be a detectable file. /// /// File size public static bool IsFileTooBig(long length) { if (length <= 0x100_0000) // 16 MB return false; if (length > int.MaxValue) return true; if (SaveUtil.IsSizeValid((int)length)) return false; if (SAV3GCMemoryCard.IsMemoryCardSize(length)) return false; // pbr/GC have size > 1MB return true; } /// /// Checks if the length is too small to be a detectable file. /// /// File size public static bool IsFileTooSmall(long length) => length < 0x20; // bigger than PK1 /// /// Tries to get a object from the input parameters. /// /// Binary data /// Output result /// True if file object reference is valid, false if none found. public static bool TryGetMemoryCard(Memory data, [NotNullWhen(true)] out SAV3GCMemoryCard? memcard) { if (!SAV3GCMemoryCard.IsMemoryCardSize(data.Span) || IsNoDataPresent(data.Span)) { memcard = null; return false; } memcard = new SAV3GCMemoryCard(data); return true; } /// public static bool TryGetMemoryCard(string file, [NotNullWhen(true)] out SAV3GCMemoryCard? memcard) { if (!File.Exists(file)) { memcard = null; return false; } var data = File.ReadAllBytes(file); return TryGetMemoryCard(data, out memcard); } /// /// Tries to get a object from the input parameters. /// /// Binary data /// Output result /// Format hint /// Reference save file used for PC Binary compatibility checks. /// True if file object reference is valid, false if none found. public static bool TryGetPKM(Memory data, [NotNullWhen(true)] out PKM? pk, ReadOnlySpan ext, ITrainerInfo? sav = null) { if (ext.EndsWith("pgt")) // size collision with pk6 { pk = null; return false; } var format = EntityFileExtension.GetContextFromExtension(ext, sav?.Context ?? EntityContext.Gen6); pk = EntityFormat.GetFromBytes(data, prefer: format); return pk is not null; } /// /// Tries to get a object from the input parameters. /// /// Binary data /// Output result /// Reference SaveFile used for PC Binary compatibility checks. /// True if file object reference is valid, false if none found. public static bool TryGetPCBoxBin(Memory data, [NotNullWhen(true)] out ConcatenatedEntitySet? result, SaveFile? sav) { result = null; if (sav is null || IsNoDataPresent(data.Span)) return false; // Only return if the size is one of the save file's data chunk formats. var expect = sav.SIZE_BOXSLOT; // Check if it's the entire PC data. var countPC = sav.SlotCount; if (expect * countPC == data.Length) { result = new(data, countPC); return true; } // Check if it's a single box data. var countBox = sav.BoxSlotCount; if (expect * countBox == data.Length) { result = new(data, countBox); return true; } return false; } private static bool IsNoDataPresent(ReadOnlySpan data) { if (!data.ContainsAnyExcept(0xFF)) return true; if (!data.ContainsAnyExcept(0x00)) return true; return false; } /// /// Tries to get a object from the input parameters. /// /// Binary data /// Output result /// True if file object reference is valid, false if none found. public static bool TryGetBattleVideo(Memory data, [NotNullWhen(true)] out IBattleVideo? bv) { bv = BattleVideo.GetVariantBattleVideo(data); return bv is not null; } /// /// Tries to get a object from the input parameters. /// /// Binary data /// Output result /// Format hint /// True if file object reference is valid, false if none found. public static bool TryGetMysteryGift(Memory data, [NotNullWhen(true)] out MysteryGift? mg, ReadOnlySpan ext) { mg = ext.Length == 0 ? MysteryGift.GetMysteryGift(data) : MysteryGift.GetMysteryGift(data, ext); return mg is not null; } /// /// Gets a Temp location File Name for the . /// /// Data to be exported /// Data is to be encrypted /// Path to temporary file location to write to. public static string GetPKMTempFileName(PKM pk, bool encrypt) { string fn = pk.FileNameWithoutExtension; string filename = fn + (encrypt ? $".ek{pk.Format}" : $".{pk.Extension}"); return Path.Combine(Path.GetTempPath(), PathUtil.CleanFileName(filename)); } /// /// Gets a from the provided path, which is to be loaded to the . /// /// or file path. /// Generation Info /// New reference from the file. public static PKM? GetSingleFromPath(string file, ITrainerInfo sav) { var fi = new FileInfo(file); if (!fi.Exists) return null; if (fi.Length == GP1.SIZE && TryGetGP1(File.ReadAllBytes(file), out var gp1)) return gp1.ConvertToPKM(sav); if (!EntityDetection.IsSizePlausible(fi.Length) && !MysteryGift.IsMysteryGift(fi.Length)) return null; var data = File.ReadAllBytes(file); var ext = fi.Extension; var mg = MysteryGift.GetMysteryGift(data, ext); var gift = mg?.ConvertToPKM(sav); if (gift is not null) return gift; _ = TryGetPKM(data, out var pk, ext, sav); return pk; } } /// /// Represents a set of concatenated data. /// /// Object data /// Count of objects public sealed record ConcatenatedEntitySet(Memory Data, int Count) { /// /// Size of each Entity in bytes. /// public int SlotSize => Data.Length / Count; /// /// Retrieves a specific slot of data from the concatenated set. /// /// Slot index to retrieve. public Span GetSlot(int index) { var size = SlotSize; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)size); var offset = index * size; return Data.Span.Slice(offset, size); } }