Skip to content
Open
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
63 changes: 62 additions & 1 deletion android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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("<!DO") || headerString.startsWith("<htm")) {
return false
}

// Check if it's JSON (API error responses)
if (headerString.startsWith("{") || headerString.startsWith("[")) {
return false
}

// If we can't definitively identify it as non-Rive, let the Rive runtime validate it
// This allows for different Rive file formats/versions
return true
}

fun setArtboardName(artboardName: String) {
try {
this.artboardName = artboardName
Expand Down
65 changes: 52 additions & 13 deletions example/app/(examples)/ErrorHandledManually.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SafeAreaView, ScrollView, StyleSheet } from 'react-native';
import { SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native';
import Rive, {
Alignment,
Fit,
Expand All @@ -10,47 +10,67 @@ export default function ErrorHandledManually() {
return (
<SafeAreaView style={styles.safeAreaViewContainer}>
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Testing Issue #327 Fix</Text>
<Text style={styles.description}>
Testing the URL that was causing MalformedFileException crashes
</Text>

<Rive
fit={Fit.Contain}
alignment={Alignment.Center}
style={styles.animation}
animationName="exampleName"
url={'wrongUrl'}
url={
'https://cdn.jsdelivr.net/gh/shubham-k2/assets@main/Updated%20surprise_sale_banner.riv'
}
onError={(riveError: RNRiveError) => {
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;
}
}}
/>

<Text style={styles.subtitle}>Testing Working URL</Text>
<Rive
fit={Fit.Contain}
alignment={Alignment.Center}
style={styles.animation}
url={'https://public.rive.app/community/runtime-files/2195-4346-avatar-pack-use-case.riv'}
onError={(riveError: RNRiveError) => {
console.log('📱 Working URL Error (unexpected):', riveError);
}}
/>
</ScrollView>
</SafeAreaView>
);
Expand All @@ -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,
},
});
2 changes: 1 addition & 1 deletion ios/RNRiveError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
64 changes: 63 additions & 1 deletion ios/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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("<!DO") || headerString.hasPrefix("<htm") {
return false
}

// Check if it's JSON (API error responses)
if headerString.hasPrefix("{") || headerString.hasPrefix("[") {
return false
}
}

// If we can't definitively identify it as non-Rive, let the Rive runtime validate it
// This allows for different Rive file formats/versions
return true
}

private func reloadView() {
if resourceFromBundle {
if requiresLocalResourceReconfigure {
Expand Down Expand Up @@ -577,7 +616,30 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
if error != nil {
self?.handleInvalidUrlError(url: url.absoluteString)
} else if let data = data {
listener(data)
// Check HTTP response status
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200...299:
listener(data)
case 404:
let error = createIncorrectRiveURL("File not found (404) at: \(url)")
self.handleRiveError(error: error)
case 403:
let error = createIncorrectRiveURL("Access forbidden (403) for: \(url)")
self.handleRiveError(error: error)
case 500...599:
let error = createIncorrectRiveURL("Server error (\(httpResponse.statusCode)) for: \(url)")
self.handleRiveError(error: error)
default:
let error = createIncorrectRiveURL("HTTP error (\(httpResponse.statusCode)) for: \(url)")
self.handleRiveError(error: error)
}
} else {
listener(data)
}
} else {
let error = createIncorrectRiveURL("No data received from: \(url)")
self.handleRiveError(error: error)
}
}

Expand Down