Skip to content

Commit b138b7f

Browse files
committed
1 parent f569362 commit b138b7f

3 files changed

Lines changed: 193 additions & 11 deletions

File tree

android/src/main/java/com/rivereactnative/RiveReactNativeView.kt

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import app.rive.runtime.kotlin.controllers.RiveFileController
1414
import app.rive.runtime.kotlin.core.*
1515
import app.rive.runtime.kotlin.core.errors.*
1616
import app.rive.runtime.kotlin.renderers.PointerEvents
17+
import com.android.volley.DefaultRetryPolicy
1718
import com.android.volley.NetworkResponse
1819
import com.android.volley.ParseError
1920
import com.android.volley.Request
@@ -716,9 +717,33 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
716717
}
717718

718719

719-
private fun setUrlRiveResource(url: String, autoplay: Boolean = this.autoplay) {
720+
private fun setUrlRiveResource(url: String) {
720721
downloadUrlAsset(url) { bytes ->
721722
try {
723+
// Validate that we have valid content before attempting to create Rive file
724+
if (bytes.isEmpty()) {
725+
if (isUserHandlingErrors) {
726+
val rnRiveError = RNRiveError.IncorrectRiveFileUrl
727+
rnRiveError.message = "Downloaded file is empty from: $url"
728+
sendErrorToRN(rnRiveError)
729+
} else {
730+
showRNRiveError("Downloaded file is empty from: $url", null)
731+
}
732+
return@downloadUrlAsset
733+
}
734+
735+
// Basic validation - check if the content starts with the Rive file signature
736+
if (!isValidRiveContent(bytes)) {
737+
if (isUserHandlingErrors) {
738+
val rnRiveError = RNRiveError.MalformedFile
739+
rnRiveError.message = "Downloaded content is not a valid Rive file from: $url"
740+
sendErrorToRN(rnRiveError)
741+
} else {
742+
showRNRiveError("Downloaded content is not a valid Rive file from: $url", null)
743+
}
744+
return@downloadUrlAsset
745+
}
746+
722747
riveAnimationView?.setRiveBytes(
723748
bytes,
724749
fit = this.fit,
@@ -737,6 +762,42 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
737762
}
738763
}
739764

765+
/**
766+
* Validates if the downloaded content is a valid Rive file by checking file signatures
767+
*/
768+
private fun isValidRiveContent(bytes: ByteArray): Boolean {
769+
if (bytes.size < 4) return false
770+
771+
// Check for Rive file signature (RIVE magic number)
772+
// Rive files start with specific byte patterns
773+
val header = bytes.take(4).toByteArray()
774+
775+
// Check for "RIVE" header (0x52495645)
776+
if (header[0] == 0x52.toByte() &&
777+
header[1] == 0x49.toByte() &&
778+
header[2] == 0x56.toByte() &&
779+
header[3] == 0x45.toByte()) {
780+
return true
781+
}
782+
783+
// Additional validation - check for common non-Rive content patterns
784+
val headerString = String(header, Charsets.UTF_8)
785+
786+
// Check if it's HTML (error pages)
787+
if (headerString.startsWith("<!DO") || headerString.startsWith("<htm")) {
788+
return false
789+
}
790+
791+
// Check if it's JSON (API error responses)
792+
if (headerString.startsWith("{") || headerString.startsWith("[")) {
793+
return false
794+
}
795+
796+
// If we can't definitively identify it as non-Rive, let the Rive runtime validate it
797+
// This allows for different Rive file formats/versions
798+
return true
799+
}
800+
740801
fun setArtboardName(artboardName: String) {
741802
try {
742803
this.artboardName = artboardName
@@ -993,11 +1054,45 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
9931054
return
9941055
}
9951056

1057+
// Properly encode the URL to handle spaces and special characters
1058+
val encodedUrl = try {
1059+
val uri = Uri.parse(url)
1060+
uri.toString() // This will properly encode the URL
1061+
} catch (e: Exception) {
1062+
handleInvalidUrlError(url)
1063+
return
1064+
}
1065+
9961066
val queue = Volley.newRequestQueue(context)
9971067

9981068
val stringRequest = RNRiveFileRequest(
999-
url, listener
1000-
) { error -> handleURLAssetError(url, error, isUserHandlingErrors) }
1069+
encodedUrl, listener
1070+
) { error ->
1071+
// Enhanced error handling for better debugging
1072+
val errorMessage = when {
1073+
error.networkResponse?.statusCode == 404 -> "File not found (404) at: $url"
1074+
error.networkResponse?.statusCode == 403 -> "Access forbidden (403) for: $url"
1075+
error.networkResponse?.statusCode == 500 -> "Server error (500) for: $url"
1076+
error.cause is java.net.SocketTimeoutException -> "Timeout downloading from: $url"
1077+
error.cause is java.net.UnknownHostException -> "Cannot resolve host for: $url"
1078+
else -> "Unable to download the Rive asset file from: $url"
1079+
}
1080+
1081+
if (isUserHandlingErrors) {
1082+
val rnRiveError = RNRiveError.IncorrectRiveFileUrl
1083+
rnRiveError.message = errorMessage
1084+
sendErrorToRN(rnRiveError)
1085+
} else {
1086+
showRNRiveError(errorMessage, error)
1087+
}
1088+
}
1089+
1090+
// Add timeout to prevent hanging requests
1091+
stringRequest.retryPolicy = DefaultRetryPolicy(
1092+
15000, // 15 second timeout
1093+
1, // no retries
1094+
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
1095+
)
10011096

10021097
queue.add(stringRequest)
10031098
}

ios/RNRiveError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func createFileNotFoundError() -> NSError {
6565
}
6666

6767
func createMalformedFileError() -> NSError {
68-
return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Malformed Rive File", "name": "Malformed"])
68+
return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Malformed Rive File - downloaded content is not a valid .riv file", "name": "Malformed"])
6969
}
7070

7171
func createAssetFileError(_ assetName: String) -> NSError {

ios/RiveReactNativeView.swift

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
352352
handleRiveError(error: createIncorrectRiveURL(url))
353353
return
354354
}
355+
356+
// Validate that we have valid Rive content before attempting to create RiveFile
357+
if !isValidRiveContent(data) {
358+
handleRiveError(error: createMalformedFileError())
359+
return
360+
}
361+
355362
do {
356363
let riveFile = try RiveFile(data: data, loadCdn: true, customAssetLoader: customLoader)
357364
let riveModel = RiveModel(riveFile: riveFile)
@@ -378,6 +385,38 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
378385
}
379386

380387
}
388+
389+
/**
390+
* Validates if the downloaded content is a valid Rive file by checking file signatures
391+
*/
392+
private func isValidRiveContent(_ data: Data) -> Bool {
393+
guard data.count >= 4 else { return false }
394+
395+
// Check for Rive file signature (RIVE magic number)
396+
let header = data.prefix(4)
397+
398+
// Check for "RIVE" header (0x52495645)
399+
if header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x56 && header[3] == 0x45 {
400+
return true
401+
}
402+
403+
// Additional validation - check for common non-Rive content patterns
404+
if let headerString = String(data: header, encoding: .utf8) {
405+
// Check if it's HTML (error pages)
406+
if headerString.hasPrefix("<!DO") || headerString.hasPrefix("<htm") {
407+
return false
408+
}
409+
410+
// Check if it's JSON (API error responses)
411+
if headerString.hasPrefix("{") || headerString.hasPrefix("[") {
412+
return false
413+
}
414+
}
415+
416+
// If we can't definitively identify it as non-Rive, let the Rive runtime validate it
417+
// This allows for different Rive file formats/versions
418+
return true
419+
}
381420

382421
private func reloadView() {
383422
if resourceFromBundle {
@@ -522,18 +561,66 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
522561
return
523562
}
524563

525-
let queue = URLSession.shared
526-
guard let requestUrl = URL(string: url) else {
564+
// Properly encode the URL to handle spaces and special characters
565+
guard let encodedUrlString = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
566+
let requestUrl = URL(string: encodedUrlString) else {
527567
handleInvalidUrlError(url: url)
528568
return
529569
}
530570

531-
let request = URLRequest(url: requestUrl)
532-
let task = queue.dataTask(with: request) {[weak self] data, response, error in
533-
if error != nil {
534-
self?.handleInvalidUrlError(url: url)
571+
let session = URLSession.shared
572+
var request = URLRequest(url: requestUrl)
573+
request.timeoutInterval = 15.0 // 15 second timeout
574+
575+
let task = session.dataTask(with: request) {[weak self] data, response, error in
576+
guard let self = self else { return }
577+
578+
if let error = error {
579+
let errorMessage: String
580+
if let urlError = error as? URLError {
581+
switch urlError.code {
582+
case .timedOut:
583+
errorMessage = "Timeout downloading from: \(url)"
584+
case .notConnectedToInternet:
585+
errorMessage = "No internet connection for: \(url)"
586+
case .cannotFindHost:
587+
errorMessage = "Cannot find host for: \(url)"
588+
case .fileDoesNotExist:
589+
errorMessage = "File not found at: \(url)"
590+
default:
591+
errorMessage = "Network error downloading from: \(url)"
592+
}
593+
} else {
594+
errorMessage = "Unknown error downloading from: \(url)"
595+
}
596+
597+
let customError = createIncorrectRiveURL(errorMessage)
598+
self.handleRiveError(error: customError)
535599
} else if let data = data {
536-
listener(data)
600+
// Check HTTP response status
601+
if let httpResponse = response as? HTTPURLResponse {
602+
switch httpResponse.statusCode {
603+
case 200...299:
604+
listener(data)
605+
case 404:
606+
let error = createIncorrectRiveURL("File not found (404) at: \(url)")
607+
self.handleRiveError(error: error)
608+
case 403:
609+
let error = createIncorrectRiveURL("Access forbidden (403) for: \(url)")
610+
self.handleRiveError(error: error)
611+
case 500...599:
612+
let error = createIncorrectRiveURL("Server error (\(httpResponse.statusCode)) for: \(url)")
613+
self.handleRiveError(error: error)
614+
default:
615+
let error = createIncorrectRiveURL("HTTP error (\(httpResponse.statusCode)) for: \(url)")
616+
self.handleRiveError(error: error)
617+
}
618+
} else {
619+
listener(data)
620+
}
621+
} else {
622+
let error = createIncorrectRiveURL("No data received from: \(url)")
623+
self.handleRiveError(error: error)
537624
}
538625
}
539626

0 commit comments

Comments
 (0)