Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -308,15 +308,15 @@ class RetroArchSetupCoordinator(
return false
}
val entry = activeEntry.get() ?: return false
val token = sessionEpoch.capture(entry.sessionIdentity()) ?: return false
val token = sessionEpoch.capture(entry.currentSessionIdentity()) ?: return false
if (!activationGate.retry(entry.sourceId)) return false
activate(entry, token)
return true
}

fun selectSave(documentId: String): Boolean {
val entry = activeEntry.get() ?: return false
val token = sessionEpoch.capture(entry.sessionIdentity()) ?: return false
val token = sessionEpoch.capture(entry.currentSessionIdentity()) ?: return false
if (lastSaveCandidates.get().none { it.id == documentId }) return false
val selected = saveMonitor.select(entry.sha256, documentId) { commit ->
sessionEpoch.commitIfCurrent(token, commit)
Expand Down Expand Up @@ -599,39 +599,39 @@ class RetroArchSetupCoordinator(
?: SessionResolution.NoContent
val connected = session.connection != RetroArchConnection.DISCONNECTED
val restartVerified = restartVerifier.observe(session.connection)
val resolvedEntry = (resolution as? SessionResolution.Resolved)?.entry
val nextAuthorizedEntry = resolvedEntry?.takeIf { connected }
val catalogCancellation = if (activeEntry.get() != nextAuthorizedEntry) {
val sourceVerificationEntry = RomSessionResolver.sourceVerificationCandidate(resolution)
?.takeIf { connected }
val catalogCancellation = if (activeEntry.get() != sourceVerificationEntry) {
runtime.cancelPendingCatalogLoadForAuthorityTransition()
} else {
null
}
val token = sessionEpoch.observe(
nextAuthorizedEntry?.sessionIdentity(),
sourceVerificationEntry?.sessionIdentity(resolution),
)
catalogCancellation?.complete()
val authorizedEntry = nextAuthorizedEntry?.takeIf { token != null }
val previousEntry = activeEntry.getAndSet(authorizedEntry)
if (previousEntry != authorizedEntry) {
val candidateEntry = sourceVerificationEntry?.takeIf { token != null }
val previousEntry = activeEntry.getAndSet(candidateEntry)
if (previousEntry != candidateEntry) {
previousEntry?.let { activationGate.cancel(it.sourceId) }
restoredSaveRom.set(null)
lastSaveCandidates.set(emptyList())
discoveredSaveRom.set(null)
discoveredSaveBasename.set(null)
}
val active = authorizedEntry != null &&
val active = candidateEntry != null &&
token != null &&
activationCoordinator.isVerified(token) &&
runtime.catalogHash() == authorizedEntry.sha256
val loading = authorizedEntry != null && token != null &&
activationCoordinator.isLoading(authorizedEntry.sourceId, token)
val failed = authorizedEntry != null && token != null &&
activationCoordinator.isFailed(authorizedEntry.sourceId, token)
runtime.catalogHash() == candidateEntry.sha256
val loading = candidateEntry != null && token != null &&
activationCoordinator.isLoading(candidateEntry.sourceId, token)
val failed = candidateEntry != null && token != null &&
activationCoordinator.isFailed(candidateEntry.sourceId, token)
val publishBattleSession = {
battleMemory.updateSession(
connected = connected && active,
systemId = status?.systemId,
romIdentity = authorizedEntry?.sha256,
romIdentity = candidateEntry?.sha256,
)
}
if (token != null) {
Expand All @@ -648,9 +648,9 @@ class RetroArchSetupCoordinator(
systemId = status?.systemId,
gameBasename = status?.gameBasename,
contentCrc32 = status?.crc32,
contentSha256 = authorizedEntry?.sha256?.takeIf { active },
contentSha256 = candidateEntry?.sha256?.takeIf { active },
sessionEpoch = token?.epoch?.takeIf { active },
activeSource = authorizedEntry?.sourceName?.takeIf { active },
activeSource = candidateEntry?.sourceName?.takeIf { active },
savefileDirectory = session.savefileDirectory,
resolution = when (resolution) {
SessionResolution.NoContent -> "NO_CONTENT"
Expand All @@ -660,17 +660,22 @@ class RetroArchSetupCoordinator(
failed -> "FAILED"
else -> "RESOLVED"
}
is SessionResolution.Unverified -> "UNVERIFIED"
is SessionResolution.Unverified -> when {
active -> "ACTIVE"
loading -> "LOADING"
failed -> "FAILED"
else -> "UNVERIFIED"
}
is SessionResolution.Ambiguous -> "AMBIGUOUS"
is SessionResolution.NotFound -> "NOT_FOUND"
},
message = session.error ?: when {
connected && active -> "Opened ${resolvedEntry.sourceName}."
connected && active -> "Opened ${sourceVerificationEntry.sourceName}."
connected && loading -> "Opening the SHA-256-verified active catalog…"
connected && failed -> current.message
connected && resolution is SessionResolution.Resolved -> "Active content matched; verifying its SHA-256."
connected && resolution is SessionResolution.Unverified ->
"A matching filename was found, but RetroArch did not provide content identity. Live features are paused."
"Active filename matched; opening and hashing the exact granted ROM source."
connected && resolution is SessionResolution.Ambiguous -> "Multiple granted sources match the active content. Select the ROM manually."
connected && resolution is SessionResolution.NotFound -> resolution.reason
connected -> "RetroArch Network Commands verified."
Expand All @@ -684,11 +689,11 @@ class RetroArchSetupCoordinator(
} else if (!closed) {
publishSessionView()
}
if (authorizedEntry != null && token != null) {
activate(authorizedEntry, token)
if (candidateEntry != null && token != null) {
activate(candidateEntry, token)
if (active) {
restorePersistedSave(authorizedEntry, token)
pollSave(authorizedEntry, token)
restorePersistedSave(candidateEntry, token)
pollSave(candidateEntry, token)
}
}
}
Expand Down Expand Up @@ -1072,9 +1077,24 @@ class RetroArchSetupCoordinator(
context.contentResolver.takePersistableUriPermission(uri, flags)
}

private fun RomIndexEntry.sessionIdentity() = VerifiedSessionIdentity(
private fun RomIndexEntry.currentSessionIdentity() = VerifiedSessionIdentity(
romSha256 = sha256.lowercase(),
sourceId = sourceId,
evidence = if (view.get().contentCrc32 == null) {
SessionIdentityEvidence.BASENAME_DISCOVERY
} else {
SessionIdentityEvidence.RETROARCH_CRC
},
)

private fun RomIndexEntry.sessionIdentity(resolution: SessionResolution) = VerifiedSessionIdentity(
romSha256 = sha256.lowercase(),
sourceId = sourceId,
evidence = when (resolution) {
is SessionResolution.Resolved -> SessionIdentityEvidence.RETROARCH_CRC
is SessionResolution.Unverified -> SessionIdentityEvidence.BASENAME_DISCOVERY
else -> error("non-candidate session resolution")
},
)

private fun connectionOf(value: String): RetroArchConnection =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.FutureTask

internal enum class SessionIdentityEvidence {
RETROARCH_CRC,
BASENAME_DISCOVERY,
}

internal data class VerifiedSessionIdentity(
val romSha256: String,
val sourceId: String,
val evidence: SessionIdentityEvidence = SessionIdentityEvidence.RETROARCH_CRC,
)

internal data class SessionWorkToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ class SessionEpochGateTest {
assertFalse(activation.isVerified(reconnect))
}

@Test
fun losingCrcEvidenceForTheSameSourceRequiresFreshVerification() {
val gate = SessionEpochGate()
val activation = SessionActivationCoordinator(gate)
val crcBacked = requireNotNull(gate.observe(first))
assertTrue(activation.begin(crcBacked, first.sourceId) {})
assertTrue(activation.finish(crcBacked, first.sourceId) {})

val discovered = first.copy(evidence = SessionIdentityEvidence.BASENAME_DISCOVERY)
val crcLess = requireNotNull(gate.observe(discovered))

assertNotEquals(crcBacked, crcLess)
assertFalse(activation.isVerified(crcLess))
assertTrue(
activation.requiresSourceVerification(
crcLess,
activeCatalogSha256 = first.romSha256,
expectedSha256 = first.romSha256,
),
)
}

@Test
fun reconnectingTheSameIdentityRequiresANewVerificationToken() {
val gate = SessionEpochGate()
Expand Down
2 changes: 1 addition & 1 deletion docs/reports/qa-hardening/stage-07-closure.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ A post-run inventory comparison found the same 333 eligible names and one intent
| Current release/governance gate after closure packaging and APK privacy correction | 85 Node tests passed. |
| Fresh corpus summarization | Streaming raw-report hash, source/generator lineage, canonical multiset, terminal outcomes, and persistence/reopen checks passed. |

The expensive parser and consolidated Gradle gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, acceptance-test stabilization, the later replacement of two Android API-33-only bounded reads with the shared API-30-compatible reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to use public nonsecret endpoints instead of an integration token lacking administration scope, binding the comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text. Focused checkpoint/save tests, the source-contract regression, Android test compilation, app lint, the PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, and the release-governance tests passed after those corrections. `NONPARSER_REUSE` is explicit and does not permit parser, catalog, build, wrapper, or corpus-execution changes.
The expensive parser and consolidated Gradle gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, acceptance-test stabilization, the later replacement of two Android API-33-only bounded reads with the shared API-30-compatible reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to use public nonsecret endpoints instead of an integration token lacking administration scope, binding the comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text, or restoring CRC-less RetroArch live activation through fresh granted-source SHA verification and evidence-bound session epochs. Focused checkpoint/save tests, the source-contract regression, Android test compilation, app lint, the PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, the focused CRC-less RetroArch resolver/session-authority/activation regressions, and the release-governance tests passed after those corrections. `NONPARSER_REUSE` is explicit and does not permit parser, catalog, build, wrapper, or corpus-execution changes.

## Missing-feature classification

Expand Down
2 changes: 1 addition & 1 deletion docs/reports/qa-hardening/stage-08-closure.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The denominator correction does not waive an input. The audited physical invento
| Final corpus | 333/333 eligible inputs terminal; 0 parser/catalog/compatibility/persistence errors; 278/278 selected catalogs persisted and reopened | `PASS` |
| Downstream evidence hardening | Bounded 1.63 GB streaming summary, corrected denominator, duplicate-aware canonical contract, promotion/readiness/privacy regressions | `PASS` |

The consolidated Gradle and hours-long parser gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, deterministic acceptance-test corrections, the replacement of two Android API-33-only read calls with the already-tested API-30-compatible bounded reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to public nonsecret endpoints, binding the release comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text. Focused checkpoint/save tests, source contracts, Android test compilation, app lint, the 7/7 PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, and the release-governance suite passed afterward. The evidence validator rejects reuse if parser/catalog sources, build logic, wrapper, or corpus-execution tooling changes.
The consolidated Gradle and hours-long parser gates ran once after parser/catalog product-source stabilization. They were not repeated for downstream release packaging, deterministic acceptance-test corrections, the replacement of two Android API-33-only read calls with the already-tested API-30-compatible bounded reader, repository-policy alignment with the established single-maintainer signing process, correction of GitHub policy reads to public nonsecret endpoints, binding the release comparison range to the cache decision's prior parser schema, tightening the Windows-path detector so arbitrary compressed APK bytes cannot be misclassified as a private path, or preserving area-title sign labels in the Local-map projection while suppressing only the duplicate Area Guide drawer text, or restoring CRC-less RetroArch live activation through fresh granted-source SHA verification and evidence-bound session epochs. Focused checkpoint/save tests, source contracts, Android test compilation, app lint, the 7/7 PR managed-device suite, the candidate workflows' complete unsigned build and packaged Android gates, the reproduced APK privacy regression, the complete Area Guide suite, the exact Modern Emerald sign control, the focused CRC-less RetroArch resolver/session-authority/activation regressions, and the release-governance suite passed afterward. The evidence validator rejects reuse if parser/catalog sources, build logic, wrapper, or corpus-execution tooling changes.

No ad hoc emulator, ADB gesture, physical-device action, credential inspection, signing-material inspection, signing, tagging, or publication was performed for this closure.

Expand Down
Loading
Loading