Cleanup FirmwareManager

This commit is contained in:
YoshiRulz 2021-02-12 16:45:09 +10:00
parent 2f18c74840
commit dc3bd050da
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
2 changed files with 111 additions and 133 deletions

View File

@ -1,4 +1,6 @@
using System; #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.IO; using System.IO;
@ -9,67 +11,73 @@ using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common namespace BizHawk.Client.Common
{ {
public class FirmwareManager public sealed class FirmwareManager
{ {
private static readonly FirmwareID NDS_FIRMWARE = new("NDS", "firmware"); private static readonly FirmwareID NDS_FIRMWARE = new("NDS", "firmware");
public List<FirmwareEventArgs> RecentlyServed { get; } = new List<FirmwareEventArgs>(); private readonly IReadOnlyCollection<long> _firmwareSizes;
private readonly List<FirmwareEventArgs> _recentlyServed = new();
private readonly Dictionary<FirmwareRecord, ResolutionInfo> _resolutionDictionary = new(); private readonly Dictionary<FirmwareRecord, ResolutionInfo> _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 ICollection<FirmwareEventArgs> RecentlyServed => _recentlyServed;
public ResolutionInfo Resolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareRecord record, bool forbidScan = false)
public FirmwareManager()
{ {
_resolutionDictionary.TryGetValue(record, out var resolved); _firmwareSizes = new HashSet<long>(FirmwareDatabase.FirmwareFiles.Select(ff => ff.Size)); // build a list of expected file sizes, used as a simple filter to speed up scanning
// couldn't find it! do a scan and resolve to try harder }
/// <remarks>
/// Sometimes this is called from a loop in <c>FirmwaresConfig.DoScan</c>.
/// In that case, we don't want to call <see cref="DoScanAndResolve"/> repeatedly, so we use <paramref name="forbidScan"/> to skip it.
/// </remarks>
public ResolutionInfo? Resolve(PathEntryCollection pathEntries, IDictionary<string, string> 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... // NOTE: this could result in bad performance in some cases if the scanning happens repeatedly...
if (resolved == null && !forbidScan) DoScanAndResolve(pathEntries, userSpecifications);
{ return _resolutionDictionary.TryGetValue(record, out var resolved1) ? resolved1 : null;
DoScanAndResolve(pathEntries, userSpecifications);
_resolutionDictionary.TryGetValue(record, out resolved);
}
return resolved;
} }
// Requests the specified firmware. tries really hard to scan and resolve as necessary // Requests the specified firmware. tries really hard to scan and resolve as necessary
public string Request(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareID id) public string? Request(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareID id)
{ {
var resolved = Resolve( var resolved = Resolve(
pathEntries, pathEntries,
userSpecifications, userSpecifications,
FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id)); FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id));
if (resolved == null) if (resolved == null) return null;
{
return null;
}
RecentlyServed.Add(new(id, resolved.Hash, resolved.Size)); RecentlyServed.Add(new(id, resolved.Hash, resolved.Size));
return resolved.FilePath; return resolved.FilePath;
} }
private class RealFirmwareReader : IDisposable private sealed class RealFirmwareReader : IDisposable
{ {
private SHA1 _sha1 = SHA1.Create(); private readonly Dictionary<string, RealFirmwareFile> _dict = new();
private SHA1? _sha1 = SHA1.Create();
public IReadOnlyDictionary<string, RealFirmwareFile> Dict => _dict;
public void Dispose() public void Dispose()
{ {
_sha1.Dispose(); _sha1?.Dispose();
_sha1 = null; _sha1 = null;
} }
public RealFirmwareFile Read(FileInfo fi) public RealFirmwareFile Read(FileInfo fi)
{ {
using (var fs = fi.OpenRead()) if (_sha1 == null) throw new ObjectDisposedException(nameof(RealFirmwareReader));
{ using var fs = fi.OpenRead();
_sha1.ComputeHash(fs); _sha1!.ComputeHash(fs);
}
var hash = _sha1.Hash.BytesToHexString(); var hash = _sha1.Hash.BytesToHexString();
var rff = new RealFirmwareFile(fi, hash); return _dict![hash] = new RealFirmwareFile(fi, hash);
Dict[hash] = rff;
return rff;
} }
public Dictionary<string, RealFirmwareFile> Dict { get; } = new Dictionary<string, RealFirmwareFile>();
} }
/// <summary> /// <summary>
@ -80,8 +88,7 @@ namespace BizHawk.Client.Common
try try
{ {
var fi = new FileInfo(f); var fi = new FileInfo(f);
if (!fi.Exists) if (!fi.Exists) return false;
return false;
// weed out filesizes first to reduce the unnecessary overhead of a hashing operation // weed out filesizes first to reduce the unnecessary overhead of a hashing operation
if (FirmwareDatabase.FirmwareFiles.All(a => a.Size != fi.Length)) return false; if (FirmwareDatabase.FirmwareFiles.All(a => a.Size != fi.Length)) return false;
@ -90,131 +97,102 @@ namespace BizHawk.Client.Common
using var reader = new RealFirmwareReader(); using var reader = new RealFirmwareReader();
reader.Read(fi); reader.Read(fi);
var hash = reader.Dict.Values.First().Hash; 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<string, string> userSpecifications) public void DoScanAndResolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications)
{ {
// build a list of file sizes. Only those will be checked during scanning
var sizes = new HashSet<long>();
foreach (var ff in FirmwareDatabase.FirmwareFiles)
{
sizes.Add(ff.Size);
}
using var reader = new RealFirmwareReader(); 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 // 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<DirectoryInfo>(); var todo = new Queue<DirectoryInfo>(new[] { new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null)) });
todo.Enqueue(new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null)));
while (todo.Count != 0) while (todo.Count != 0)
{ {
var di = todo.Dequeue(); var di = todo.Dequeue();
if (!di.Exists) continue;
if (!di.Exists) foreach (var subDir in di.GetDirectories()) todo.Enqueue(subDir); // recurse
{
continue;
}
// we're going to allow recursing into subdirectories, now. its been verified to work OK foreach (var fi in di.GetFiles().Where(fi => _firmwareSizes.Contains(fi.Length))) reader.Read(fi);
foreach (var subDir in di.GetDirectories())
{
todo.Enqueue(subDir);
}
foreach (var fi in di.GetFiles())
{
if (sizes.Contains(fi.Length))
{
reader.Read(fi);
}
}
} }
// now, for each firmware record, try to resolve it // now, for each firmware record, try to resolve it
foreach (var fr in FirmwareDatabase.FirmwareRecords) foreach (var fr in FirmwareDatabase.FirmwareRecords)
{ {
// clear previous resolution results _resolutionDictionary.Remove(fr); // clear previous resolution results
_resolutionDictionary.Remove(fr); FirmwareOption fo;
try
// 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)
{ {
var hash = fo.Hash; // 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
// did we find this firmware? && reader.Dict.ContainsKey(fo1.Hash));
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;
}
} }
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 // apply user overrides
foreach (var fr in FirmwareDatabase.FirmwareRecords) foreach (var fr in FirmwareDatabase.FirmwareRecords)
{ {
// do we have a user specification for this firmware record? // 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 ri = new ResolutionInfo();
if (!_resolutionDictionary.TryGetValue(fr, out ResolutionInfo ri)) _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(); ri.KnownMismatching = true;
_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;
}
} }
} }
} // foreach(firmware record) }
} // DoScanAndResolve() }
} // class FirmwareManager }
} // namespace }

View File

@ -235,7 +235,7 @@ namespace BizHawk.Client.Common
} }
} }
if (firmwareManager.RecentlyServed.Any()) if (firmwareManager.RecentlyServed.Count != 0)
{ {
foreach (var firmware in firmwareManager.RecentlyServed) foreach (var firmware in firmwareManager.RecentlyServed)
{ {