diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt index 4078d65d..9a164215 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 @@ -720,9 +721,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, @@ -741,6 +766,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(" + 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/ios/RNRiveError.swift b/ios/RNRiveError.swift index 690bbd47..ce5f30dd 100644 --- a/ios/RNRiveError.swift +++ b/ios/RNRiveError.swift @@ -66,7 +66,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 381a8201..08896d78 100644 --- a/ios/RiveReactNativeView.swift +++ b/ios/RiveReactNativeView.swift @@ -353,6 +353,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) @@ -380,6 +387,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("