289 lines
7.9 KiB
C#
289 lines
7.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Security.Cryptography;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
using BizHawk.Common.BufferExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
|
|
namespace BizHawk.Client.Common
|
|
{
|
|
public class FirmwareManager
|
|
{
|
|
// represents a file found on disk in the user's firmware directory matching a file in our database
|
|
public class RealFirmwareFile
|
|
{
|
|
public FileInfo FileInfo { get; set; }
|
|
public string Hash { get; set; }
|
|
}
|
|
|
|
public List<FirmwareEventArgs> RecentlyServed { get; }
|
|
|
|
public class ResolutionInfo
|
|
{
|
|
public bool UserSpecified { get; set; }
|
|
public bool Missing { get; set; }
|
|
public bool KnownMismatching { get; set; }
|
|
public FirmwareDatabase.FirmwareFile KnownFirmwareFile { get; set; }
|
|
public string FilePath { get; set; }
|
|
public string Hash { get; set; }
|
|
public long Size { get; set; }
|
|
}
|
|
|
|
private readonly Dictionary<FirmwareDatabase.FirmwareRecord, ResolutionInfo> _resolutionDictionary = new Dictionary<FirmwareDatabase.FirmwareRecord, ResolutionInfo>();
|
|
|
|
public class FirmwareEventArgs
|
|
{
|
|
public string Hash { get; set; }
|
|
public long Size { get; set; }
|
|
public string SystemId { get; set; }
|
|
public string FirmwareId { get; set; }
|
|
}
|
|
|
|
public FirmwareManager()
|
|
{
|
|
RecentlyServed = new List<FirmwareEventArgs>();
|
|
}
|
|
|
|
public ResolutionInfo Resolve(string sysId, string firmwareId)
|
|
{
|
|
return Resolve(FirmwareDatabase.LookupFirmwareRecord(sysId, firmwareId));
|
|
}
|
|
|
|
public ResolutionInfo Resolve(FirmwareDatabase.FirmwareRecord record, bool forbidScan = false)
|
|
{
|
|
// purpose of forbidScan: sometimes this is called from a loop in Scan(). we dont want to repeatedly DoScanAndResolve in that case, its already been done.
|
|
bool first = true;
|
|
|
|
RETRY:
|
|
ResolutionInfo resolved;
|
|
_resolutionDictionary.TryGetValue(record, out resolved);
|
|
|
|
// couldnt find it! do a scan and resolve to try harder
|
|
// NOTE: this could result in bad performance in some cases if the scanning happens repeatedly..
|
|
if (resolved == null && first)
|
|
{
|
|
if (!forbidScan)
|
|
{
|
|
DoScanAndResolve();
|
|
}
|
|
|
|
first = false;
|
|
goto RETRY;
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
// Requests the spcified firmware. tries really hard to scan and resolve as necessary
|
|
public string Request(string sysId, string firmwareId)
|
|
{
|
|
var resolved = Resolve(sysId, firmwareId);
|
|
if (resolved == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
RecentlyServed.Add(new FirmwareEventArgs
|
|
{
|
|
SystemId = sysId,
|
|
FirmwareId = firmwareId,
|
|
Hash = resolved.Hash,
|
|
Size = resolved.Size
|
|
});
|
|
|
|
return resolved.FilePath;
|
|
}
|
|
|
|
private class RealFirmwareReader : IDisposable
|
|
{
|
|
private readonly List<RealFirmwareFile> _files = new List<RealFirmwareFile>();
|
|
private SHA1 _sha1 = SHA1.Create();
|
|
|
|
public void Dispose()
|
|
{
|
|
_sha1.Dispose();
|
|
_sha1 = null;
|
|
}
|
|
|
|
public RealFirmwareFile Read(FileInfo fi)
|
|
{
|
|
var rff = new RealFirmwareFile { FileInfo = fi };
|
|
long len = fi.Length;
|
|
|
|
using (var fs = fi.OpenRead())
|
|
{
|
|
_sha1.ComputeHash(fs);
|
|
}
|
|
|
|
rff.Hash = _sha1.Hash.BytesToHexString();
|
|
Dict[rff.Hash] = rff;
|
|
_files.Add(rff);
|
|
return rff;
|
|
}
|
|
|
|
public Dictionary<string, RealFirmwareFile> Dict { get; } = new Dictionary<string, RealFirmwareFile>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to determine whether the supplied firmware file matches something in the firmware database
|
|
/// </summary>
|
|
public bool CanFileBeImported(string f)
|
|
{
|
|
try
|
|
{
|
|
var fi = new FileInfo(f);
|
|
if (!fi.Exists)
|
|
return false;
|
|
|
|
// weed out filesizes first to reduce the unnecessary overhead of a hashing operation
|
|
if (FirmwareDatabase.FirmwareFiles.Where(a => a.Size == fi.Length).FirstOrDefault() == null)
|
|
return false;
|
|
|
|
// check the hash
|
|
using (var reader = new RealFirmwareReader())
|
|
{
|
|
reader.Read(fi);
|
|
if (FirmwareDatabase.FirmwareFiles.Where(a => a.Hash == reader.Dict.FirstOrDefault().Value.Hash).FirstOrDefault() != null)
|
|
return true;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
return false;
|
|
}
|
|
|
|
public void DoScanAndResolve()
|
|
{
|
|
// build a list of file sizes. Only those will be checked during scanning
|
|
HashSet<long> sizes = new HashSet<long>();
|
|
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<DirectoryInfo>();
|
|
todo.Enqueue(new DirectoryInfo(PathManager.MakeAbsolutePath(Global.Config.PathEntries.FirmwaresPathFragment, null)));
|
|
|
|
while (todo.Count != 0)
|
|
{
|
|
var di = todo.Dequeue();
|
|
|
|
if (!di.Exists)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// we're going to allow recursing into subdirectories, now. its been verified to work OK
|
|
foreach (var disub in di.GetDirectories())
|
|
{
|
|
todo.Enqueue(disub);
|
|
}
|
|
|
|
foreach (var fi in di.GetFiles())
|
|
{
|
|
if (sizes.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 fr1 = fr;
|
|
var options =
|
|
from fo in FirmwareDatabase.FirmwareOptions
|
|
where fo.SystemId == fr1.SystemId && fo.FirmwareId == fr1.FirmwareId && fo.IsAcceptableOrIdeal
|
|
select fo;
|
|
|
|
// try each option
|
|
foreach (var fo in options)
|
|
{
|
|
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;
|
|
goto DONE_FIRMWARE;
|
|
}
|
|
}
|
|
|
|
DONE_FIRMWARE: ;
|
|
}
|
|
|
|
// apply user overrides
|
|
foreach (var fr in FirmwareDatabase.FirmwareRecords)
|
|
{
|
|
string userSpec;
|
|
|
|
// do we have a user specification for this firmware record?
|
|
if (Global.Config.FirmwareUserSpecifications.TryGetValue(fr.ConfigKey, out userSpec))
|
|
{
|
|
// flag it as user specified
|
|
ResolutionInfo ri;
|
|
if (!_resolutionDictionary.TryGetValue(fr, out ri))
|
|
{
|
|
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
|
|
var rff = reader.Read(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 doesnt really use this information right now)
|
|
FirmwareDatabase.FirmwareFile ff;
|
|
if (FirmwareDatabase.FirmwareFilesByHash.TryGetValue(rff.Hash, out ff))
|
|
{
|
|
ri.KnownFirmwareFile = ff;
|
|
|
|
// if the known firmware file is for a different firmware, flag it so we can show a warning
|
|
var option =
|
|
(from fo in FirmwareDatabase.FirmwareOptions
|
|
where fo.Hash == rff.Hash && fo.ConfigKey != fr.ConfigKey
|
|
select fr).FirstOrDefault();
|
|
|
|
if (option != null)
|
|
{
|
|
ri.KnownMismatching = true;
|
|
}
|
|
}
|
|
}
|
|
} // foreach(firmware record)
|
|
} // using(new RealFirmwareReader())
|
|
} // DoScanAndResolve()
|
|
} // class FirmwareManager
|
|
} // namespace |