setup wizard check hash, name and file sizes

This commit is contained in:
izzy2lost
2026-03-13 18:06:50 -04:00
parent 6f9300b5aa
commit 5d637a99ab
2 changed files with 245 additions and 24 deletions
@@ -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())
}
+12 -2
View File
@@ -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>