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. /// /// /// 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(byte[] data, ReadOnlySpan ext, SaveFile? reference = null) { if (TryGetSAV(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 } 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(byte[] data, [NotNullWhen(true)] out GP1? gp1) { gp1 = null; if (data.Length != GP1.SIZE || ReadUInt32LittleEndian(data.AsSpan(0x28)) == 0) return false; gp1 = new GP1(data); return true; } private static bool TryGetBundle(byte[] 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 <= 0x10_0000) // 1 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 TryGetSAV(byte[] data, [NotNullWhen(true)] out SaveFile? sav) { sav = SaveUtil.GetVariantSAV(data); return sav != null; } /// /// 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(byte[] data, [NotNullWhen(true)] out SAV3GCMemoryCard? memcard) { if (!SAV3GCMemoryCard.IsMemoryCardSize(data) || IsNoDataPresent(data)) { 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 savefile used for PC Binary compatibility checks. /// True if file object reference is valid, false if none found. public static bool TryGetPKM(byte[] data, [NotNullWhen(true)] out PKM? pk, ReadOnlySpan ext, ITrainerInfo? sav = null) { if (ext == ".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 != 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(byte[] data, [NotNullWhen(true)] out ConcatenatedEntitySet? result, SaveFile? sav) { result = null; if (sav is null || IsNoDataPresent(data)) 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(byte[] data, [NotNullWhen(true)] out IBattleVideo? bv) { bv = BattleVideo.GetVariantBattleVideo(data); return bv != 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(byte[] data, [NotNullWhen(true)] out MysteryGift? mg, ReadOnlySpan ext) { mg = ext.Length == 0 ? MysteryGift.GetMysteryGift(data) : MysteryGift.GetMysteryGift(data, ext); return mg != 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(), Util.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 != 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) { public int SlotSize => Data.Length / Count; public Span GetSlot(int index) { var size = SlotSize; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)size); var offset = index * size; return Data.Span.Slice(offset, size); } }