From 61d551c091f4f7442324167606b41a110f5723bd Mon Sep 17 00:00:00 2001 From: Mahesh Maharana Date: Tue, 29 Jul 2025 21:44:50 +0530 Subject: [PATCH] fix malformed rive file crash --- .../rivereactnative/RiveReactNativeView.kt | 101 +++++++++++++++++- .../app/(examples)/ErrorHandledManually.tsx | 65 ++++++++--- example/ios/Podfile.lock | 18 ++-- ios/RNRiveError.swift | 2 +- ios/RiveReactNativeView.swift | 101 ++++++++++++++++-- 5 files changed, 254 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt index 25746a83..9f9bf3a9 100644 --- a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt +++ b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt @@ -14,6 +14,7 @@ import app.rive.runtime.kotlin.controllers.RiveFileController import app.rive.runtime.kotlin.core.* import app.rive.runtime.kotlin.core.errors.* import app.rive.runtime.kotlin.renderers.PointerEvents +import com.android.volley.DefaultRetryPolicy import com.android.volley.NetworkResponse import com.android.volley.ParseError import com.android.volley.Request @@ -716,9 +717,33 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } - private fun setUrlRiveResource(url: String, autoplay: Boolean = this.autoplay) { + private fun setUrlRiveResource(url: String) { downloadUrlAsset(url) { bytes -> try { + // Validate that we have valid content before attempting to create Rive file + if (bytes.isEmpty()) { + if (isUserHandlingErrors) { + val rnRiveError = RNRiveError.IncorrectRiveFileUrl + rnRiveError.message = "Downloaded file is empty from: $url" + sendErrorToRN(rnRiveError) + } else { + showRNRiveError("Downloaded file is empty from: $url", null) + } + return@downloadUrlAsset + } + + // Basic validation - check if the content starts with the Rive file signature + if (!isValidRiveContent(bytes)) { + if (isUserHandlingErrors) { + val rnRiveError = RNRiveError.MalformedFile + rnRiveError.message = "Downloaded content is not a valid Rive file from: $url" + sendErrorToRN(rnRiveError) + } else { + showRNRiveError("Downloaded content is not a valid Rive file from: $url", null) + } + return@downloadUrlAsset + } + riveAnimationView?.setRiveBytes( bytes, fit = this.fit, @@ -737,6 +762,42 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } } + /** + * Validates if the downloaded content is a valid Rive file by checking file signatures + */ + private fun isValidRiveContent(bytes: ByteArray): Boolean { + if (bytes.size < 4) return false + + // Check for Rive file signature (RIVE magic number) + // Rive files start with specific byte patterns + val header = bytes.take(4).toByteArray() + + // Check for "RIVE" header (0x52495645) + if (header[0] == 0x52.toByte() && + header[1] == 0x49.toByte() && + header[2] == 0x56.toByte() && + header[3] == 0x45.toByte()) { + return true + } + + // Additional validation - check for common non-Rive content patterns + val headerString = String(header, Charsets.UTF_8) + + // Check if it's HTML (error pages) + if (headerString.startsWith(" handleURLAssetError(url, error, isUserHandlingErrors) } + encodedUrl, listener + ) { error -> + // Enhanced error handling for better debugging + val errorMessage = when { + error.networkResponse?.statusCode == 404 -> "File not found (404) at: $url" + error.networkResponse?.statusCode == 403 -> "Access forbidden (403) for: $url" + error.networkResponse?.statusCode == 500 -> "Server error (500) for: $url" + error.cause is java.net.SocketTimeoutException -> "Timeout downloading from: $url" + error.cause is java.net.UnknownHostException -> "Cannot resolve host for: $url" + else -> "Unable to download the Rive asset file from: $url" + } + + if (isUserHandlingErrors) { + val rnRiveError = RNRiveError.IncorrectRiveFileUrl + rnRiveError.message = errorMessage + sendErrorToRN(rnRiveError) + } else { + showRNRiveError(errorMessage, error) + } + } + + // Add timeout to prevent hanging requests + stringRequest.retryPolicy = DefaultRetryPolicy( + 15000, // 15 second timeout + 1, // no retries + DefaultRetryPolicy.DEFAULT_BACKOFF_MULT + ) queue.add(stringRequest) } diff --git a/example/app/(examples)/ErrorHandledManually.tsx b/example/app/(examples)/ErrorHandledManually.tsx index 217e33cd..8fd862c3 100644 --- a/example/app/(examples)/ErrorHandledManually.tsx +++ b/example/app/(examples)/ErrorHandledManually.tsx @@ -1,4 +1,4 @@ -import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; +import { SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native'; import Rive, { Alignment, Fit, @@ -10,47 +10,67 @@ export default function ErrorHandledManually() { return ( + Testing Issue #327 Fix + + Testing the URL that was causing MalformedFileException crashes + + { + console.log('📱 Rive Error Received:', riveError); switch (riveError.type) { case RNRiveErrorType.IncorrectRiveFileUrl: { - console.log(`${riveError.message}`); + console.log(`❌ URL Error: ${riveError.message}`); return; } case RNRiveErrorType.MalformedFile: { - console.log('Malformed File'); + console.log(`❌ Malformed File: ${riveError.message}`); return; } case RNRiveErrorType.FileNotFound: { - console.log('File not found'); + console.log(`❌ File not found: ${riveError.message}`); return; } case RNRiveErrorType.IncorrectArtboardName: { - console.log('IncorrectAnimationName'); + console.log(`❌ Incorrect Artboard: ${riveError.message}`); return; } case RNRiveErrorType.UnsupportedRuntimeVersion: { - console.log('Runtime version unsupported'); + console.log(`❌ Runtime version unsupported: ${riveError.message}`); return; } case RNRiveErrorType.IncorrectStateMachineName: { - console.log(`${riveError.message}`); + console.log(`❌ State Machine Error: ${riveError.message}`); return; } case RNRiveErrorType.IncorrectStateMachineInput: { - console.log(`${riveError.message}`); + console.log(`❌ State Machine Input Error: ${riveError.message}`); return; } default: + console.log(`❌ Unknown Error: ${riveError.message}`); return; } }} /> + + Testing Working URL + { + console.log('📱 Working URL Error (unexpected):', riveError); + }} + /> ); @@ -62,11 +82,30 @@ const styles = StyleSheet.create({ }, container: { flexGrow: 1, - alignItems: 'center', - justifyContent: 'center', + padding: 20, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + marginBottom: 10, + textAlign: 'center', + }, + subtitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 20, + marginBottom: 10, + textAlign: 'center', + }, + description: { + fontSize: 14, + marginBottom: 20, + textAlign: 'center', + color: '#666', }, animation: { - width: '100%', - height: 600, + width: 200, + height: 200, + marginBottom: 20, }, }); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bd9f0864..e044efc3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -55,9 +55,9 @@ PODS: - FBLazyVector (0.76.7) - fmt (9.1.0) - glog (0.3.5) - - hermes-engine (0.76.9): - - hermes-engine/Pre-built (= 0.76.9) - - hermes-engine/Pre-built (0.76.9) + - hermes-engine (0.76.7): + - hermes-engine/Pre-built (= 0.76.7) + - hermes-engine/Pre-built (0.76.7) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1690,10 +1690,10 @@ PODS: - React-logger (= 0.76.7) - React-perflogger (= 0.76.7) - React-utils (= 0.76.7) - - rive-react-native (9.3.2): + - rive-react-native (9.3.4): - React-Core - - RiveRuntime (= 6.9.3) - - RiveRuntime (6.9.3) + - RiveRuntime (= 6.9.4) + - RiveRuntime (6.9.4) - RNCPicker (2.11.0): - DoubleConversion - glog @@ -2162,7 +2162,7 @@ SPEC CHECKSUMS: FBLazyVector: ca8044c9df513671c85167838b4188791b6f37e1 fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a - hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 + hermes-engine: eb4a80f6bf578536c58a44198ec93a30f6e69218 RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 RCTDeprecation: 7691283dd69fed46f6653d376de6fa83aaad774c RCTRequired: eac044a04629288f272ee6706e31f81f3a2b4bfe @@ -2222,8 +2222,8 @@ SPEC CHECKSUMS: React-utils: 0342746d2cf989cf5e0d1b84c98cfa152edbdf3f ReactCodegen: e1c019dc68733dd2c5d3b263b4a6dc72002c0045 ReactCommon: 81e0744ee33adfd6d586141b927024f488bc49ea - rive-react-native: 725272a66939f4b1a9e7c5c357c110bf0e6616b1 - RiveRuntime: e13fde45c994d11f8b65af5b6d2fe821e971fb0c + rive-react-native: 9c7100fec9480d23dc69b4f4fb321de6bb742c4a + RiveRuntime: 56c2133fa5c5c570dd93818d8c2772ae6f126806 RNCPicker: c657bd58a82b164a957812f82a0b4bab4245de2e RNGestureHandler: 16ef3dc2d7ecb09f240f25df5255953c4098819b RNReanimated: a2692304a6568bc656c04c8ffea812887d37436e diff --git a/ios/RNRiveError.swift b/ios/RNRiveError.swift index ae953fcb..601abc49 100644 --- a/ios/RNRiveError.swift +++ b/ios/RNRiveError.swift @@ -65,7 +65,7 @@ func createFileNotFoundError() -> NSError { } func createMalformedFileError() -> NSError { - return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Malformed Rive File", "name": "Malformed"]) + return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Malformed Rive File - downloaded content is not a valid .riv file", "name": "Malformed"]) } func createAssetFileError(_ assetName: String) -> NSError { diff --git a/ios/RiveReactNativeView.swift b/ios/RiveReactNativeView.swift index 4a218ed1..743a287c 100644 --- a/ios/RiveReactNativeView.swift +++ b/ios/RiveReactNativeView.swift @@ -352,6 +352,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate handleRiveError(error: createIncorrectRiveURL(url)) return } + + // Validate that we have valid Rive content before attempting to create RiveFile + if !isValidRiveContent(data) { + handleRiveError(error: createMalformedFileError()) + return + } + do { let riveFile = try RiveFile(data: data, loadCdn: true, customAssetLoader: customLoader) let riveModel = RiveModel(riveFile: riveFile) @@ -379,6 +386,38 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } + /** + * Validates if the downloaded content is a valid Rive file by checking file signatures + */ + private func isValidRiveContent(_ data: Data) -> Bool { + guard data.count >= 4 else { return false } + + // Check for Rive file signature (RIVE magic number) + let header = data.prefix(4) + + // Check for "RIVE" header (0x52495645) + if header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x56 && header[3] == 0x45 { + return true + } + + // Additional validation - check for common non-Rive content patterns + if let headerString = String(data: header, encoding: .utf8) { + // Check if it's HTML (error pages) + if headerString.hasPrefix("