diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/OpenCodePayload.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/OpenCodePayload.kt index 2e251ced6..7416e1d38 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/OpenCodePayload.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/OpenCodePayload.kt @@ -53,7 +53,13 @@ data class OpenCodePayload( } val kind = PayloadKind.from(frame[0].toInt()) - return OpenCodePayload(kind, kind.decode(frame), kind.decodeNonce(frame)) + + // `decode` returns null for a frame this kind can't parse — e.g. a foreign or corrupt + // Kik code whose currency byte is out of range (Bugsnag 6a5a4523: + // IndexOutOfBoundsException on CurrencyCode.entries[232], a 171-entry enum). Treat it + // as Empty so the scanner ignores it instead of crashing or attempting a bogus grab. + val value = kind.decode(frame) ?: return Empty + return OpenCodePayload(kind, value, kind.decodeNonce(frame)) } } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/PayloadKind.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/PayloadKind.kt index b81f1ca01..14725b9cd 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/PayloadKind.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/PayloadKind.kt @@ -19,8 +19,11 @@ sealed interface PayloadKind { /** Encodes [value] (and [nonce], where applicable) into the fixed-length scan frame. */ fun encode(value: PayloadValue, nonce: List): List - /** Reads this kind's [PayloadValue] out of a (zero-padded) scan [frame]. */ - fun decode(frame: List): PayloadValue + /** + * Reads this kind's [PayloadValue] out of a (zero-padded) scan [frame], or null if the frame + * can't be parsed as this kind (e.g. a cash frame whose currency byte is out of range). + */ + fun decode(frame: List): PayloadValue? /** Reads the nonce out of a scan [frame]; empty for kinds that carry none. */ fun decodeNonce(frame: List): List @@ -45,8 +48,11 @@ sealed interface PayloadKind { return data } - override fun decode(frame: List): PayloadValue { - val currency = CurrencyCode.entries[frame[1].byteToUnsignedInt()] + override fun decode(frame: List): PayloadValue? { + // Byte 1 is an index into CurrencyCode. A foreign or corrupt Kik code can carry a byte + // outside that range; such a frame isn't a valid cash code, so fail decoding rather + // than throw (Bugsnag 6a5a4523: IndexOutOfBoundsException on a 171-entry enum). + val currency = CurrencyCode.entries.getOrNull(frame[1].byteToUnsignedInt()) ?: return null val quarks = frame.subList(OpenCodePayload.OFFSET_QUARKS, OpenCodePayload.OFFSET_NONCE) .toByteArray().byteArrayToLong() return Fiat(currencyCode = currency, quarks = quarks) diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/model/core/OpenCodePayloadTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/model/core/OpenCodePayloadTest.kt index 75f3f2db0..50cc30427 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/model/core/OpenCodePayloadTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/model/core/OpenCodePayloadTest.kt @@ -92,6 +92,34 @@ class OpenCodePayloadTest { assertIs(decoded.value) } + @Test + fun `fromList does not throw on out-of-range currency byte`() { + // A foreign / corrupt Kik code can pass error-correction yet carry a currency byte + // outside CurrencyCode's range. Regression for IndexOutOfBoundsException (Bugsnag + // 6a5a4523): `CurrencyCode.entries[232]` on a 171-element enum. Such a frame is not a + // valid Flipcash cash code, so it must decode to Unknown (which the scanner ignores). + val frame = MutableList(OpenCodePayload.LENGTH) { 0 } + frame[0] = PayloadKind.Cash.value.toByte() + frame[1] = 232.toByte() // >= CurrencyCode.entries.size + + val decoded = OpenCodePayload.fromList(frame) + + assertEquals(PayloadKind.Unknown, decoded.kind) + } + + @Test + fun `fromList decodes highest valid currency index`() { + // Boundary: the last valid ordinal must still decode as Cash, not be rejected. + val lastCurrency = CurrencyCode.entries.last() + val fiat = Fiat(quarks = 7L, currencyCode = lastCurrency) + val encoded = encodePayload(PayloadKind.Cash, fiat, List(10) { 0.toByte() }) + + val decoded = OpenCodePayload.fromList(encoded) + + assertEquals(PayloadKind.Cash, decoded.kind) + assertEquals(lastCurrency, decoded.fiat!!.currencyCode) + } + @Test fun `fromList preserves nonce bytes`() { val nonce = List(10) { (it * 3).toByte() }