mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
setup wizard check hash, name and file sizes
This commit is contained in:
@@ -3,6 +3,7 @@ package com.izzy2lost.x1box
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
@@ -15,8 +16,22 @@ import com.google.android.material.button.MaterialButton
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
class SetupWizardActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val EXPECTED_MCPX_MD5 = "d49c52a4102f6df7bcf8d0617ac475ed"
|
||||
private const val KNOWN_BAD_MCPX_MD5 = "196a5f59a13382c185636e691d6c323d"
|
||||
private const val EXPECTED_MCPX_SIZE_BYTES = 512L
|
||||
}
|
||||
|
||||
private data class FileFingerprint(
|
||||
val displayName: String,
|
||||
val sizeBytes: Long,
|
||||
val md5: String,
|
||||
)
|
||||
|
||||
private val prefs by lazy { getSharedPreferences("x1box_prefs", MODE_PRIVATE) }
|
||||
|
||||
private lateinit var pageMcpx: View
|
||||
@@ -57,16 +72,19 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
showExtensionError(mcpxExts)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
persistUriPermission(uri)
|
||||
mcpxUri = uri
|
||||
prefs.edit().putString("mcpxUri", uri.toString()).apply()
|
||||
copyUriAsync(uri, "mcpx.bin") { path ->
|
||||
if (path != null) {
|
||||
mcpxPath = path
|
||||
prefs.edit().putString("mcpxPath", path).apply()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to copy MCPX ROM", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
copyValidatedCoreFileAsync(
|
||||
uri = uri,
|
||||
destName = "mcpx.bin",
|
||||
validator = ::validateSelectedMcpxFile,
|
||||
) { path ->
|
||||
persistUriPermission(uri)
|
||||
mcpxUri = uri
|
||||
mcpxPath = path
|
||||
prefs.edit()
|
||||
.putString("mcpxUri", uri.toString())
|
||||
.putString("mcpxPath", path)
|
||||
.apply()
|
||||
Toast.makeText(this, R.string.setup_mcpx_verified, Toast.LENGTH_SHORT).show()
|
||||
updateMcpxSelection()
|
||||
updateButtons()
|
||||
}
|
||||
@@ -80,16 +98,18 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
showExtensionError(flashExts)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
persistUriPermission(uri)
|
||||
flashUri = uri
|
||||
prefs.edit().putString("flashUri", uri.toString()).apply()
|
||||
copyUriAsync(uri, "flash.bin") { path ->
|
||||
if (path != null) {
|
||||
flashPath = path
|
||||
prefs.edit().putString("flashPath", path).apply()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to copy flash ROM", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
copyValidatedCoreFileAsync(
|
||||
uri = uri,
|
||||
destName = "flash.bin",
|
||||
validator = ::validateSelectedFlashFile,
|
||||
) { path ->
|
||||
persistUriPermission(uri)
|
||||
flashUri = uri
|
||||
flashPath = path
|
||||
prefs.edit()
|
||||
.putString("flashUri", uri.toString())
|
||||
.putString("flashPath", path)
|
||||
.apply()
|
||||
updateFlashSelection()
|
||||
updateButtons()
|
||||
}
|
||||
@@ -133,8 +153,8 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mcpxPath = loadLocalPath("mcpxPath")
|
||||
flashPath = loadLocalPath("flashPath")
|
||||
mcpxPath = loadValidatedLocalPath("mcpxPath", "mcpxUri", ::isSavedMcpxFileValid)
|
||||
flashPath = loadValidatedLocalPath("flashPath", "flashUri", ::isSavedFlashFileValid)
|
||||
hddPath = loadLocalPath("hddPath")
|
||||
mcpxUri = prefs.getString("mcpxUri", null)?.let(Uri::parse)
|
||||
flashUri = prefs.getString("flashUri", null)?.let(Uri::parse)
|
||||
@@ -308,6 +328,23 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
return path
|
||||
}
|
||||
|
||||
private fun loadValidatedLocalPath(
|
||||
pathKey: String,
|
||||
uriKey: String,
|
||||
validator: (File) -> Boolean,
|
||||
): String? {
|
||||
val path = loadLocalPath(pathKey) ?: return null
|
||||
val file = File(path)
|
||||
if (validator(file)) {
|
||||
return path
|
||||
}
|
||||
prefs.edit()
|
||||
.remove(pathKey)
|
||||
.remove(uriKey)
|
||||
.apply()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isFileReady(path: String?): Boolean {
|
||||
return path != null && File(path).isFile
|
||||
}
|
||||
@@ -370,6 +407,180 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyValidatedCoreFileAsync(
|
||||
uri: Uri,
|
||||
destName: String,
|
||||
validator: (FileFingerprint) -> String?,
|
||||
onSuccess: (String) -> Unit,
|
||||
) {
|
||||
if (isCopying) return
|
||||
isCopying = true
|
||||
updateButtons()
|
||||
Toast.makeText(this, R.string.setup_validating_file, Toast.LENGTH_SHORT).show()
|
||||
Thread {
|
||||
val fingerprint = readFingerprint(uri)
|
||||
val validationError = if (fingerprint != null) {
|
||||
validator(fingerprint)
|
||||
} else {
|
||||
getString(R.string.setup_file_validation_failed)
|
||||
}
|
||||
val copiedPath = if (fingerprint != null && validationError == null) {
|
||||
copyUriToAppStorage(uri, destName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
isCopying = false
|
||||
when {
|
||||
fingerprint == null -> {
|
||||
Toast.makeText(this, R.string.setup_file_validation_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
validationError != null -> {
|
||||
Toast.makeText(this, validationError, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
copiedPath != null -> {
|
||||
onSuccess(copiedPath)
|
||||
return@runOnUiThread
|
||||
}
|
||||
destName == "mcpx.bin" -> {
|
||||
Toast.makeText(this, R.string.setup_mcpx_copy_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(this, R.string.setup_flash_copy_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
updateMcpxSelection()
|
||||
updateFlashSelection()
|
||||
updateButtons()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun validateSelectedMcpxFile(fingerprint: FileFingerprint): String? {
|
||||
if (fingerprint.md5 == EXPECTED_MCPX_MD5) {
|
||||
return null
|
||||
}
|
||||
if (fingerprint.md5 == KNOWN_BAD_MCPX_MD5) {
|
||||
return getString(R.string.setup_mcpx_invalid_bad_dump, EXPECTED_MCPX_MD5)
|
||||
}
|
||||
|
||||
val sizeLabel = Formatter.formatShortFileSize(this, fingerprint.sizeBytes)
|
||||
return if (fingerprint.sizeBytes != EXPECTED_MCPX_SIZE_BYTES) {
|
||||
getString(
|
||||
R.string.setup_mcpx_invalid_size,
|
||||
fingerprint.displayName,
|
||||
sizeLabel,
|
||||
EXPECTED_MCPX_MD5,
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.setup_mcpx_invalid_hash,
|
||||
fingerprint.displayName,
|
||||
fingerprint.md5,
|
||||
EXPECTED_MCPX_MD5,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSelectedFlashFile(fingerprint: FileFingerprint): String? {
|
||||
if (fingerprint.sizeBytes == EXPECTED_MCPX_SIZE_BYTES ||
|
||||
fingerprint.md5 == EXPECTED_MCPX_MD5 ||
|
||||
fingerprint.md5 == KNOWN_BAD_MCPX_MD5
|
||||
) {
|
||||
return getString(R.string.setup_flash_invalid_mcpx, fingerprint.displayName)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isSavedMcpxFileValid(file: File): Boolean {
|
||||
val fingerprint = readFingerprint(file) ?: return false
|
||||
return validateSelectedMcpxFile(fingerprint) == null
|
||||
}
|
||||
|
||||
private fun isSavedFlashFileValid(file: File): Boolean {
|
||||
val fingerprint = readFingerprint(file) ?: return false
|
||||
return validateSelectedFlashFile(fingerprint) == null
|
||||
}
|
||||
|
||||
private fun readFingerprint(uri: Uri): FileFingerprint? {
|
||||
val displayName = queryDisplayName(uri)
|
||||
return try {
|
||||
contentResolver.openInputStream(uri)?.use { input ->
|
||||
buildFingerprint(displayName) { digest ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var total = 0L
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) {
|
||||
break
|
||||
}
|
||||
if (read == 0) {
|
||||
continue
|
||||
}
|
||||
digest.update(buffer, 0, read)
|
||||
total += read.toLong()
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFingerprint(file: File): FileFingerprint? {
|
||||
return try {
|
||||
file.inputStream().use { input ->
|
||||
buildFingerprint(file.name) { digest ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var total = 0L
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) {
|
||||
break
|
||||
}
|
||||
if (read == 0) {
|
||||
continue
|
||||
}
|
||||
digest.update(buffer, 0, read)
|
||||
total += read.toLong()
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFingerprint(
|
||||
displayName: String?,
|
||||
reader: (MessageDigest) -> Long,
|
||||
): FileFingerprint {
|
||||
val digest = MessageDigest.getInstance("MD5")
|
||||
val sizeBytes = reader(digest)
|
||||
val md5 = digest.digest().joinToString(separator = "") { byte ->
|
||||
String.format(Locale.US, "%02x", byte.toInt() and 0xFF)
|
||||
}
|
||||
return FileFingerprint(
|
||||
displayName = displayName ?: getString(R.string.setup_selected_file_fallback_name),
|
||||
sizeBytes = sizeBytes,
|
||||
md5 = md5,
|
||||
)
|
||||
}
|
||||
|
||||
private fun queryDisplayName(uri: Uri): String? {
|
||||
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex >= 0 && cursor.moveToFirst()) {
|
||||
cursor.getString(nameIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: uri.lastPathSegment
|
||||
}
|
||||
|
||||
private fun isAllowedExtension(uri: Uri, allowed: Set<String>): Boolean {
|
||||
return isAllowedFile(uri, allowed, emptySet())
|
||||
}
|
||||
|
||||
@@ -3,19 +3,29 @@
|
||||
<string name="setup_title">Welcome to X1 BOX</string>
|
||||
<string name="setup_subtitle">Locate your original Xbox files.</string>
|
||||
<string name="setup_not_set">Not set</string>
|
||||
<string name="setup_selected_file_fallback_name">selected file</string>
|
||||
<string name="setup_validating_file">Checking file...</string>
|
||||
<string name="setup_file_validation_failed">Failed to read the selected file.</string>
|
||||
<string name="setup_back">Back</string>
|
||||
<string name="setup_next">Next</string>
|
||||
<string name="setup_finish">Finish</string>
|
||||
|
||||
<string name="setup_mcpx_title">MCPX Boot ROM</string>
|
||||
<string name="setup_mcpx_body">Choose the MCPX boot ROM file for your console.</string>
|
||||
<string name="setup_mcpx_body">Choose the MCPX boot ROM file for your console. The wizard verifies the required 512-byte mcpx_1.0.bin MD5.</string>
|
||||
<string name="setup_pick_mcpx">Choose MCPX ROM</string>
|
||||
<string name="setup_mcpx_value">MCPX ROM: %1$s</string>
|
||||
<string name="setup_mcpx_verified">Verified MCPX boot ROM</string>
|
||||
<string name="setup_mcpx_copy_failed">Failed to copy MCPX ROM</string>
|
||||
<string name="setup_mcpx_invalid_size">%1$s is %2$s, so it does not look like the required MCPX boot ROM. xemu expects the 512-byte mcpx_1.0.bin with MD5 %3$s.</string>
|
||||
<string name="setup_mcpx_invalid_hash">%1$s is 512 B, but its MD5 is %2$s. xemu expects %3$s for mcpx_1.0.bin.</string>
|
||||
<string name="setup_mcpx_invalid_bad_dump">This MCPX dump matches the known bad off-bytes hash reported by xemu. Re-dump it; xemu expects MD5 %1$s.</string>
|
||||
|
||||
<string name="setup_flash_title">Flash ROM (BIOS)</string>
|
||||
<string name="setup_flash_body">Choose the flash ROM / BIOS image.</string>
|
||||
<string name="setup_flash_body">Choose the flash ROM / BIOS image. This is separate from the 512-byte MCPX boot ROM.</string>
|
||||
<string name="setup_pick_flash">Choose Flash ROM</string>
|
||||
<string name="setup_flash_value">Flash ROM: %1$s</string>
|
||||
<string name="setup_flash_copy_failed">Failed to copy flash ROM</string>
|
||||
<string name="setup_flash_invalid_mcpx">%1$s looks like the MCPX boot ROM, not a BIOS. Pick a flash ROM / BIOS image instead.</string>
|
||||
|
||||
<string name="setup_hdd_title">Hard Disk</string>
|
||||
<string name="setup_hdd_body">Choose your Xbox hard disk image.</string>
|
||||
|
||||
Reference in New Issue
Block a user