Description
I noticed that Jp2kDecoderAsync calls evaluateJavaScriptAsync in five places but only the WASM-init path (L166) guards against unexpected results via a sentinel check. The other four call sites - precache, executeGetSize, executeDecodeImage, and getMemoryUsage - immediately pass the raw result into JSONObject(), which throws an opaque JSONException ("End of input at character 0") if the engine returns an empty string "".
The Jetpack JavaScriptEngine silently coerces non-string JS expression results (numbers, booleans, objects, undefined) to "". Since this is the async/callback variant (as opposed to Jp2kDecoder.kt), the confusing JSONException surfaces via callback.onError, making it especially hard to diagnose.
Evidence
1. WASM init correctly guards with a sentinel
In Jp2kDecoderAsync.kt (L162-L167), loadWasm checks against INTERNAL_RESULT_SUCCESS:
val resultFuture = isolate.evaluateJavaScriptAsync(script) // L162
try {
val result = resultFuture.get() // L165
if (result != INTERNAL_RESULT_SUCCESS) { // L166
throw IllegalStateException("WASM instantiation failed.")
}
An empty string is correctly caught here because "" != "1".
2. precache parses JSON on a potentially empty result
In Jp2kDecoderAsync.kt (L215-L219):
val resultFuture = isolate.evaluateJavaScriptAsync(script) // L215
val result = resultFuture.get() // L216
if (result != INTERNAL_RESULT_SUCCESS) { // L218
val root = JSONObject(result) // JSONException if "" // L219
If the engine returns "", it enters the != INTERNAL_RESULT_SUCCESS branch and JSONObject("") throws JSONException.
3. executeGetSize parses JSON directly
In Jp2kDecoderAsync.kt (L303-L307):
val resultFuture = isolate.evaluateJavaScriptAsync(script) // L303
val jsonResult =
resultFuture.get() ?: throw IllegalStateException("Result Future is null") // L305
val root = JSONObject(jsonResult) // JSONException if "" // L307
The null check on L305 does not catch "" - only null.
4. executeDecodeImage parses JSON directly
In Jp2kDecoderAsync.kt (L766-L772):
val resultFuture = isolate.evaluateJavaScriptAsync(script) // L766
val jsonResult =
resultFuture.get() ?: throw IllegalStateException("Result Future is null") // L770
val root = JSONObject(jsonResult) // JSONException if "" // L772
Same pattern: null check present, empty-string check absent.
5. getMemoryUsage parses JSON directly
In Jp2kDecoderAsync.kt (L912-L916):
val resultFuture = isolate.evaluateJavaScriptAsync("globalThis.getMemoryUsage()") // L912
val jsonResult =
resultFuture.get() ?: throw IllegalStateException("Result Future is null") // L915
val root = JSONObject(jsonResult) // JSONException if "" // L916
Why this may matter
- Four of five
evaluateJavaScriptAsync call sites would surface JSONException with "End of input at character 0" rather than a descriptive error, making diagnosis difficult.
- Since this is the async variant, errors arrive via
callback.onError where the caller has less context about which stage failed.
- The WASM-init path (L166) already demonstrates the correct pattern of checking against a known sentinel before attempting JSON parsing.
Suggested hardening
Add an empty-string check before JSON parsing, consistent with the sentinel pattern already used in loadWasm:
val jsonResult = resultFuture.get()
if (jsonResult.isNullOrEmpty()) {
throw IllegalStateException("JS engine returned empty result - expression may have evaluated to a non-string type")
}
val root = JSONObject(jsonResult)
Description
I noticed that
Jp2kDecoderAsynccallsevaluateJavaScriptAsyncin five places but only the WASM-init path (L166) guards against unexpected results via a sentinel check. The other four call sites -precache,executeGetSize,executeDecodeImage, andgetMemoryUsage- immediately pass the raw result intoJSONObject(), which throws an opaqueJSONException("End of input at character 0") if the engine returns an empty string"".The Jetpack
JavaScriptEnginesilently coerces non-string JS expression results (numbers, booleans, objects,undefined) to"". Since this is the async/callback variant (as opposed toJp2kDecoder.kt), the confusingJSONExceptionsurfaces viacallback.onError, making it especially hard to diagnose.Evidence
1. WASM init correctly guards with a sentinel
In
Jp2kDecoderAsync.kt(L162-L167),loadWasmchecks againstINTERNAL_RESULT_SUCCESS:An empty string is correctly caught here because
"" != "1".2.
precacheparses JSON on a potentially empty resultIn
Jp2kDecoderAsync.kt(L215-L219):If the engine returns
"", it enters the!= INTERNAL_RESULT_SUCCESSbranch andJSONObject("")throwsJSONException.3.
executeGetSizeparses JSON directlyIn
Jp2kDecoderAsync.kt(L303-L307):The null check on L305 does not catch
""- onlynull.4.
executeDecodeImageparses JSON directlyIn
Jp2kDecoderAsync.kt(L766-L772):Same pattern: null check present, empty-string check absent.
5.
getMemoryUsageparses JSON directlyIn
Jp2kDecoderAsync.kt(L912-L916):Why this may matter
evaluateJavaScriptAsynccall sites would surfaceJSONExceptionwith "End of input at character 0" rather than a descriptive error, making diagnosis difficult.callback.onErrorwhere the caller has less context about which stage failed.Suggested hardening
Add an empty-string check before JSON parsing, consistent with the sentinel pattern already used in
loadWasm: