diff --git a/src/BizHawk.Client.Common/fwmanager/FirmwareManager.cs b/src/BizHawk.Client.Common/fwmanager/FirmwareManager.cs index a77f62843c..afe5cd8fd4 100644 --- a/src/BizHawk.Client.Common/fwmanager/FirmwareManager.cs +++ b/src/BizHawk.Client.Common/fwmanager/FirmwareManager.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Security.Cryptography; using System.IO; @@ -9,67 +11,73 @@ using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { - public class FirmwareManager + public sealed class FirmwareManager { private static readonly FirmwareID NDS_FIRMWARE = new("NDS", "firmware"); - public List RecentlyServed { get; } = new List(); + private readonly IReadOnlyCollection _firmwareSizes; + + private readonly List _recentlyServed = new(); private readonly Dictionary _resolutionDictionary = new(); - // purpose of forbidScan: sometimes this is called from a loop in Scan(). we don't want to repeatedly DoScanAndResolve in that case, its already been done. - public ResolutionInfo Resolve(PathEntryCollection pathEntries, IDictionary userSpecifications, FirmwareRecord record, bool forbidScan = false) + public ICollection RecentlyServed => _recentlyServed; + + public FirmwareManager() { - _resolutionDictionary.TryGetValue(record, out var resolved); - // couldn't find it! do a scan and resolve to try harder + _firmwareSizes = new HashSet(FirmwareDatabase.FirmwareFiles.Select(ff => ff.Size)); // build a list of expected file sizes, used as a simple filter to speed up scanning + } + + /// + /// Sometimes this is called from a loop in FirmwaresConfig.DoScan. + /// In that case, we don't want to call repeatedly, so we use to skip it. + /// + public ResolutionInfo? Resolve(PathEntryCollection pathEntries, IDictionary userSpecifications, FirmwareRecord record, bool forbidScan = false) + { + if (_resolutionDictionary.TryGetValue(record, out var resolved)) return resolved; + // else couldn't find it + + if (forbidScan) return null; + // try harder by doing a scan and resolve // NOTE: this could result in bad performance in some cases if the scanning happens repeatedly... - if (resolved == null && !forbidScan) - { - DoScanAndResolve(pathEntries, userSpecifications); - _resolutionDictionary.TryGetValue(record, out resolved); - } - return resolved; + DoScanAndResolve(pathEntries, userSpecifications); + return _resolutionDictionary.TryGetValue(record, out var resolved1) ? resolved1 : null; } // Requests the specified firmware. tries really hard to scan and resolve as necessary - public string Request(PathEntryCollection pathEntries, IDictionary userSpecifications, FirmwareID id) + public string? Request(PathEntryCollection pathEntries, IDictionary userSpecifications, FirmwareID id) { var resolved = Resolve( pathEntries, userSpecifications, FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id)); - if (resolved == null) - { - return null; - } + if (resolved == null) return null; RecentlyServed.Add(new(id, resolved.Hash, resolved.Size)); return resolved.FilePath; } - private class RealFirmwareReader : IDisposable + private sealed class RealFirmwareReader : IDisposable { - private SHA1 _sha1 = SHA1.Create(); + private readonly Dictionary _dict = new(); + + private SHA1? _sha1 = SHA1.Create(); + + public IReadOnlyDictionary Dict => _dict; public void Dispose() { - _sha1.Dispose(); + _sha1?.Dispose(); _sha1 = null; } public RealFirmwareFile Read(FileInfo fi) { - using (var fs = fi.OpenRead()) - { - _sha1.ComputeHash(fs); - } - + if (_sha1 == null) throw new ObjectDisposedException(nameof(RealFirmwareReader)); + using var fs = fi.OpenRead(); + _sha1!.ComputeHash(fs); var hash = _sha1.Hash.BytesToHexString(); - var rff = new RealFirmwareFile(fi, hash); - Dict[hash] = rff; - return rff; + return _dict![hash] = new RealFirmwareFile(fi, hash); } - - public Dictionary Dict { get; } = new Dictionary(); } /// @@ -80,8 +88,7 @@ namespace BizHawk.Client.Common try { var fi = new FileInfo(f); - if (!fi.Exists) - return false; + if (!fi.Exists) return false; // weed out filesizes first to reduce the unnecessary overhead of a hashing operation if (FirmwareDatabase.FirmwareFiles.All(a => a.Size != fi.Length)) return false; @@ -90,131 +97,102 @@ namespace BizHawk.Client.Common using var reader = new RealFirmwareReader(); reader.Read(fi); var hash = reader.Dict.Values.First().Hash; - if (FirmwareDatabase.FirmwareFiles.Any(a => a.Hash == hash)) return true; + return FirmwareDatabase.FirmwareFiles.Any(a => a.Hash == hash); + } + catch + { + return false; } - catch { } - - return false; } public void DoScanAndResolve(PathEntryCollection pathEntries, IDictionary userSpecifications) { - // build a list of file sizes. Only those will be checked during scanning - var sizes = new HashSet(); - foreach (var ff in FirmwareDatabase.FirmwareFiles) - { - sizes.Add(ff.Size); - } - using var reader = new RealFirmwareReader(); - // build a list of files under the global firmwares path, and build a hash for each of them while we're at it - var todo = new Queue(); - todo.Enqueue(new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null))); - + // build a list of files under the global firmwares path, and build a hash for each of them (as ResolutionInfo) while we're at it + var todo = new Queue(new[] { new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null)) }); while (todo.Count != 0) { var di = todo.Dequeue(); + if (!di.Exists) continue; - if (!di.Exists) - { - continue; - } + foreach (var subDir in di.GetDirectories()) todo.Enqueue(subDir); // recurse - // we're going to allow recursing into subdirectories, now. its been verified to work OK - foreach (var subDir in di.GetDirectories()) - { - todo.Enqueue(subDir); - } - - foreach (var fi in di.GetFiles()) - { - if (sizes.Contains(fi.Length)) - { - reader.Read(fi); - } - } + foreach (var fi in di.GetFiles().Where(fi => _firmwareSizes.Contains(fi.Length))) reader.Read(fi); } // now, for each firmware record, try to resolve it foreach (var fr in FirmwareDatabase.FirmwareRecords) { - // clear previous resolution results - _resolutionDictionary.Remove(fr); - - // get all options for this firmware (in order) - var id = fr.ID; - var options = FirmwareDatabase.FirmwareOptions.Where(fo => fo.ID == id && fo.IsAcceptableOrIdeal); - - // try each option - foreach (var fo in options) + _resolutionDictionary.Remove(fr); // clear previous resolution results + FirmwareOption fo; + try { - var hash = fo.Hash; - - // did we find this firmware? - if (reader.Dict.ContainsKey(hash)) - { - // rad! then we can use it - var ri = new ResolutionInfo - { - FilePath = reader.Dict[hash].FileInfo.FullName, - KnownFirmwareFile = FirmwareDatabase.FirmwareFilesByHash[hash], - Hash = hash, - Size = fo.Size - }; - _resolutionDictionary[fr] = ri; - break; - } + // check each acceptable option for this firmware, looking for the first that's in the reader's file list + fo = FirmwareDatabase.FirmwareOptions.First(fo1 => fo1.ID == fr.ID && fo1.IsAcceptableOrIdeal + && reader.Dict.ContainsKey(fo1.Hash)); } + catch (InvalidOperationException) + { + continue; // didn't find any of them + } + // else found one, add it to the dict + _resolutionDictionary[fr] = new ResolutionInfo + { + FilePath = reader.Dict[fo.Hash].FileInfo.FullName, + KnownFirmwareFile = FirmwareDatabase.FirmwareFilesByHash[fo.Hash], + Hash = fo.Hash, + Size = fo.Size + }; } // apply user overrides foreach (var fr in FirmwareDatabase.FirmwareRecords) { // do we have a user specification for this firmware record? - if (userSpecifications.TryGetValue(fr.ID.ConfigKey, out var userSpec)) + if (!userSpecifications.TryGetValue(fr.ID.ConfigKey, out var userSpec)) continue; + + if (!_resolutionDictionary.TryGetValue(fr, out var ri)) { - // flag it as user specified - if (!_resolutionDictionary.TryGetValue(fr, out ResolutionInfo ri)) + ri = new ResolutionInfo(); + _resolutionDictionary[fr] = ri; + } + // local ri is a reference to a ResolutionInfo which is now definitely in the dict + + // flag it as user specified + ri.UserSpecified = true; + ri.KnownFirmwareFile = null; + ri.FilePath = userSpec; + ri.Hash = null; + + // check whether it exists + var fi = new FileInfo(userSpec); + if (!fi.Exists) + { + ri.Missing = true; + continue; + } + + // compute its hash + // NDS's firmware file contains user settings; these are over-written by sync settings, so we shouldn't allow them to impact the hash + var rff = reader.Read(fr.ID == NDS_FIRMWARE + ? new FileInfo(Emulation.Cores.Consoles.Nintendo.NDS.MelonDS.CreateModifiedFirmware(userSpec)) + : fi); + ri.Size = fi.Length; + ri.Hash = rff.Hash; + + // check whether it was a known file anyway, and go ahead and bind to the known file, as a perk (the firmwares config doesn't really use this information right now) + if (FirmwareDatabase.FirmwareFilesByHash.TryGetValue(rff.Hash, out var ff)) + { + ri.KnownFirmwareFile = ff; + + // if the known firmware file is for a different firmware, flag it so we can show a warning + if (FirmwareDatabase.FirmwareOptions.Any(fo => fo.Hash == rff.Hash && fo.ID != fr.ID)) { - ri = new ResolutionInfo(); - _resolutionDictionary[fr] = ri; - } - - ri.UserSpecified = true; - ri.KnownFirmwareFile = null; - ri.FilePath = userSpec; - ri.Hash = null; - - // check whether it exists - var fi = new FileInfo(userSpec); - if (!fi.Exists) - { - ri.Missing = true; - continue; - } - - // compute its hash - // NDS's firmware file contains user settings; these are over-written by sync settings, so we shouldn't allow them to impact the hash - var rff = reader.Read(fr.ID == NDS_FIRMWARE - ? new FileInfo(Emulation.Cores.Consoles.Nintendo.NDS.MelonDS.CreateModifiedFirmware(userSpec)) - : fi); - ri.Size = fi.Length; - ri.Hash = rff.Hash; - - // check whether it was a known file anyway, and go ahead and bind to the known file, as a perk (the firmwares config doesn't really use this information right now) - if (FirmwareDatabase.FirmwareFilesByHash.TryGetValue(rff.Hash, out var ff)) - { - ri.KnownFirmwareFile = ff; - - // if the known firmware file is for a different firmware, flag it so we can show a warning - if (FirmwareDatabase.FirmwareOptions.Any(fo => fo.Hash == rff.Hash && fo.ID != fr.ID)) - { - ri.KnownMismatching = true; - } + ri.KnownMismatching = true; } } - } // foreach(firmware record) - } // DoScanAndResolve() - } // class FirmwareManager -} // namespace \ No newline at end of file + } + } + } +} diff --git a/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs b/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs index b37116ea99..cc5c55cc0a 100644 --- a/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs +++ b/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs @@ -235,7 +235,7 @@ namespace BizHawk.Client.Common } } - if (firmwareManager.RecentlyServed.Any()) + if (firmwareManager.RecentlyServed.Count != 0) { foreach (var firmware in firmwareManager.RecentlyServed) {