Validates melonDS BIOS files

Compares file sizes and MD5 hashes (when relevant) to ensure BIOS files are correct.
This commit is contained in:
Riley Testut 2021-01-19 12:44:54 -06:00
parent 826cf7b0b1
commit 3e0a983048
3 changed files with 231 additions and 40 deletions

View File

@ -22,6 +22,7 @@
/* Begin PBXBuildFile section */
1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; };
BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF00BEA525B758AA00C8607D /* SystemBIOS.swift */; };
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */; };
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; };
BF1020E31F95B05B00313182 /* DeltaToDelta2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF1020E21F95B05B00313182 /* DeltaToDelta2.xcmappingmodel */; };
@ -157,6 +158,7 @@
/* Begin PBXFileReference section */
22506DA00971C4300AF90A35 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = "<group>"; };
BF00BEA525B758AA00C8607D /* SystemBIOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBIOS.swift; sourceTree = "<group>"; };
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = "<group>"; };
BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinsViewController.swift; sourceTree = "<group>"; };
@ -718,6 +720,7 @@
isa = PBXGroup;
children = (
BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */,
BF00BEA525B758AA00C8607D /* SystemBIOS.swift */,
);
path = Cores;
sourceTree = "<group>";
@ -1142,6 +1145,7 @@
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */,
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */,
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */,
BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */,
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */,
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */,

View File

@ -9,6 +9,7 @@
import UIKit
import SafariServices
import MobileCoreServices
import CryptoKit
import DeltaCore
import MelonDSDeltaCore
@ -27,44 +28,77 @@ private extension MelonDSCoreSettingsViewController
case changeCore
}
enum DSBIOS: Int
enum BIOSError: LocalizedError
{
case bios7
case bios9
case firmware
case unknownSize(URL)
case incorrectHash(URL, hash: String, expectedHash: String)
var fileURL: URL {
@available(iOS 13, *)
case incorrectSize(URL, size: Int, validSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>>)
private static let byteFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
formatter.includesActualByteCount = true
formatter.countStyle = .binary
return formatter
}()
var errorDescription: String? {
switch self
{
case .bios7: return MelonDSEmulatorBridge.shared.bios7URL
case .bios9: return MelonDSEmulatorBridge.shared.bios9URL
case .firmware: return MelonDSEmulatorBridge.shared.firmwareURL
case .unknownSize(let fileURL):
return String(format: NSLocalizedString("%@s size could not be determined.", comment: ""), fileURL.lastPathComponent)
case .incorrectHash(let fileURL, let md5Hash, let expectedHash):
return String(format: NSLocalizedString("%@s hash does not match the expected hash.\n\nHash:\n%@\n\nExpected:\n%@.", comment: ""), fileURL.lastPathComponent, md5Hash, expectedHash)
case .incorrectSize(let fileURL, let size, let validSizes):
let actualSize = BIOSError.byteFormatter.string(fromByteCount: Int64(size))
if let range = validSizes.first, validSizes.count == 1
{
if range.lowerBound == range.upperBound
{
// Single value
let expectedSize = BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
return String(format: NSLocalizedString("%@ is %@, but expected size is %@.", comment: ""), fileURL.lastPathComponent, actualSize, expectedSize)
}
else
{
// Range
BIOSError.byteFormatter.includesActualByteCount = false
defer { BIOSError.byteFormatter.includesActualByteCount = true }
let lowerBound = BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
let upperBound = BIOSError.byteFormatter.string(fromByteCount: Int64(range.upperBound.converted(to: .bytes).value))
return String(format: NSLocalizedString("%@ is %@, but expected size is between %@ and %@.", comment: ""), fileURL.lastPathComponent, actualSize, lowerBound, upperBound)
}
}
else
{
var description = String(format: NSLocalizedString("%@ is %@, but expected sizes are:", comment: ""), fileURL.lastPathComponent, actualSize) + "\n"
let sortedRanges = validSizes.sorted(by: { $0.lowerBound < $1.lowerBound })
for range in sortedRanges
{
// Assume BIOS with multiple valid file sizes don't use (>1 count) ranges.
description += "\n" + BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
}
return description
}
}
}
}
enum DSiBIOS: Int
{
case bios7
case bios9
case firmware
case nand
var fileURL: URL {
switch self
{
case .bios7: return MelonDSEmulatorBridge.shared.dsiBIOS7URL
case .bios9: return MelonDSEmulatorBridge.shared.dsiBIOS9URL
case .firmware: return MelonDSEmulatorBridge.shared.dsiFirmwareURL
case .nand: return MelonDSEmulatorBridge.shared.dsiNANDURL
}
var recoverySuggestion: String? {
return NSLocalizedString("Please choose a different BIOS file.", comment: "")
}
}
}
class MelonDSCoreSettingsViewController: UITableViewController
{
private var importDestinationURL: URL?
private var importingBIOS: SystemBIOS?
override func viewDidLoad()
{
@ -121,9 +155,9 @@ private extension MelonDSCoreSettingsViewController
self.present(safariViewController, animated: true, completion: nil)
}
func locateBIOS(for destinationURL: URL)
func locate<BIOS: SystemBIOS>(_ bios: BIOS)
{
self.importDestinationURL = destinationURL
self.importingBIOS = bios
var supportedTypes = [kUTTypeItem as String, kUTTypeContent as String, "com.apple.macbinary-archive" /* System UTI for .bin */]
@ -220,7 +254,7 @@ extension MelonDSCoreSettingsViewController
cell.contentView.isHidden = (item == nil)
case .dsBIOS:
let bios = DSBIOS(rawValue: indexPath.row)!
let bios = DSBIOS.allCases[indexPath.row]
if FileManager.default.fileExists(atPath: bios.fileURL.path)
{
@ -238,7 +272,7 @@ extension MelonDSCoreSettingsViewController
cell.selectionStyle = .default
case .dsiBIOS:
let bios = DSiBIOS(rawValue: indexPath.row)!
let bios = DSiBIOS.allCases[indexPath.row]
if FileManager.default.fileExists(atPath: bios.fileURL.path)
{
@ -288,12 +322,12 @@ extension MelonDSCoreSettingsViewController
self.openMetadataURL(for: key)
case .dsBIOS:
let bios = DSBIOS(rawValue: indexPath.row)!
self.locateBIOS(for: bios.fileURL)
let bios = DSBIOS.allCases[indexPath.row]
self.locate(bios)
case .dsiBIOS:
let bios = DSiBIOS(rawValue: indexPath.row)!
self.locateBIOS(for: bios.fileURL)
let bios = DSiBIOS.allCases[indexPath.row]
self.locate(bios)
case .changeCore:
self.changeCore()
@ -374,28 +408,58 @@ extension MelonDSCoreSettingsViewController: UIDocumentPickerDelegate
{
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
{
self.importDestinationURL = nil
self.importingBIOS = nil
self.tableView.reloadData() // Reloading index path causes cell to disappear...
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
{
defer {
self.importDestinationURL = nil
self.importingBIOS = nil
self.tableView.reloadData() // Reloading index path causes cell to disappear...
}
guard let fileURL = urls.first, let destinationURL = self.importDestinationURL else { return }
guard let fileURL = urls.first, let bios = self.importingBIOS else { return }
defer { try? FileManager.default.removeItem(at: fileURL) }
do
{
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
if #available(iOS 13.0, *)
{
// Validate file size first (since that's easiest for users to understand).
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
guard let fileSize = attributes[.size] as? Int else { throw BIOSError.unknownSize(fileURL) }
let measurement = Measurement<UnitInformationStorage>(value: Double(fileSize), unit: .bytes)
guard bios.validFileSizes.contains(where: { $0.contains(measurement) }) else { throw BIOSError.incorrectSize(fileURL, size: fileSize, validSizes: bios.validFileSizes) }
if let expectedMD5Hash = bios.expectedMD5Hash
{
// If there's an expected hash, make sure it matches.
let data = try Data(contentsOf: fileURL)
let md5Hash = Insecure.MD5.hash(data: data)
let hashString = md5Hash.compactMap { String(format: "%02x", $0) }.joined()
guard hashString == expectedMD5Hash else { throw BIOSError.incorrectHash(fileURL, hash: hashString, expectedHash: expectedMD5Hash) }
}
}
try FileManager.default.copyItem(at: fileURL, to: bios.fileURL, shouldReplace: true)
}
catch
catch let error as NSError
{
let title = String(format: NSLocalizedString("Could not import %@.", comment: ""), fileURL.lastPathComponent)
let title = String(format: NSLocalizedString("Could not import %@.", comment: ""), bios.filename)
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
var message = error.localizedDescription
if let recoverySuggestion = error.localizedRecoverySuggestion
{
message += "\n\n" + recoverySuggestion
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil)
}

View File

@ -0,0 +1,123 @@
//
// SystemBIOS.swift
// Delta
//
// Created by Riley Testut on 1/19/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import Foundation
import MelonDSDeltaCore
protocol SystemBIOS
{
var fileURL: URL { get }
var filename: String { get }
var expectedMD5Hash: String? { get }
// RangeSet would be preferable, but it's not in Swift stdlib yet.
@available(iOS 13, *)
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> { get }
}
extension SystemBIOS
{
var filename: String {
return self.fileURL.lastPathComponent
}
}
enum DSBIOS: SystemBIOS, CaseIterable
{
case bios7
case bios9
case firmware
var fileURL: URL {
switch self
{
case .bios7: return MelonDSEmulatorBridge.shared.bios7URL
case .bios9: return MelonDSEmulatorBridge.shared.bios9URL
case .firmware: return MelonDSEmulatorBridge.shared.firmwareURL
}
}
var expectedMD5Hash: String? {
switch self
{
case .bios7: return "df692a80a5b1bc90728bc3dfc76cd948"
case .bios9: return "a392174eb3e572fed6447e956bde4b25"
case .firmware: return nil
}
}
@available(iOS 13, *)
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> {
// From http://melonds.kuribo64.net/faq.php
switch self
{
case .bios7:
// 16KB
return Set([16].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
case .bios9:
// 4KB
return Set([4].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
case .firmware:
// 128KB, 256KB, or 512KB
return Set([128, 256, 512].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
}
}
}
enum DSiBIOS: SystemBIOS, CaseIterable
{
case bios7
case bios9
case firmware
case nand
var fileURL: URL {
switch self
{
case .bios7: return MelonDSEmulatorBridge.shared.dsiBIOS7URL
case .bios9: return MelonDSEmulatorBridge.shared.dsiBIOS9URL
case .firmware: return MelonDSEmulatorBridge.shared.dsiFirmwareURL
case .nand: return MelonDSEmulatorBridge.shared.dsiNANDURL
}
}
var expectedMD5Hash: String? {
switch self
{
case .bios7: return "559dae4ea78eb9d67702c56c1d791e81"
case .bios9: return "87b665fce118f76251271c3732532777"
case .firmware: return nil
case .nand: return nil
}
}
@available(iOS 13, *)
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> {
// From http://melonds.kuribo64.net/faq.php
switch self
{
case .bios7:
// 64KB
return Set([64].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
case .bios9:
// 64KB
return Set([64].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
case .firmware:
// 128KB
return Set([128].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
case .nand:
// 200MB - 300MB
return Set([200...300].map { Measurement(value: $0.lowerBound, unit: .mebibytes) ... Measurement(value: $0.upperBound, unit: .mebibytes) })
}
}
}