From 3f784ec1def0b45e76dc934e32c06564ed0b9bf4 Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Thu, 20 Nov 2025 23:12:30 +0800 Subject: [PATCH] Refine FFI error handling --- .../com/hippo/ehviewer/client/EhEngine.kt | 41 +++++++++------- .../ehviewer/client/exception/EhError.kt | 48 +++++++++++++++++++ app/src/main/rust/src/ffi/jvm.rs | 27 +++++++---- app/src/main/rust/src/lib.rs | 46 ++++++++++++++---- app/src/main/rust/src/parser/detail.rs | 4 +- 5 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/client/exception/EhError.kt diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt index 8fb778c66..91a0136d7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt @@ -31,6 +31,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.client.data.fillInfo import com.hippo.ehviewer.client.data.filterComments import com.hippo.ehviewer.client.exception.CloudflareBypassException +import com.hippo.ehviewer.client.exception.EhError import com.hippo.ehviewer.client.exception.EhException import com.hippo.ehviewer.client.exception.InsufficientFundsException import com.hippo.ehviewer.client.exception.InsufficientGpException @@ -69,6 +70,7 @@ import io.ktor.client.statement.HttpStatement import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.request import io.ktor.http.Url +import io.ktor.util.moveToByteArray import io.ktor.utils.io.pool.DirectByteBufferPool import io.ktor.utils.io.pool.useInstance import io.ktor.utils.io.readAvailable @@ -79,6 +81,8 @@ import kotlin.system.measureTimeMillis import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.serialization.SerializationException +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.json.add import kotlinx.serialization.json.addJsonArray import kotlinx.serialization.json.put @@ -127,27 +131,30 @@ private fun rethrowExactly(response: HttpResponse, body: Either throw NoHitsFoundException() - "1" -> throw EhException(R.string.gallery_list_empty_hit_subscription) - "2" -> throw NotLoggedInException() - "3" -> throw NoHathClientException() - "4" -> throw InsufficientFundsException() - is String if message.isNotEmpty() -> { - when (message[0]) { - '5' -> throw IpBannedException(message.substring(1)) - '6' -> throw EhException(message.substring(1)) - } - } - } - } + // Parse error from rust + val error = body.getOrNull()?.runCatching { + val array = moveToByteArray() + rewind() + Cbor.decodeFromByteArray(array) + }?.getOrNull() + + // Check Gallery Not Available - 404 + if (error is EhError.GalleryUnavailable) throw EhException(error.message) // Check bad response code response.status.ensureSuccess() + when (error) { + EhError.NoHits -> throw NoHitsFoundException() + EhError.NoWatched -> throw EhException(R.string.gallery_list_empty_hit_subscription) + EhError.NeedLogin -> throw NotLoggedInException() + EhError.NoHathClient -> throw NoHathClientException() + EhError.InsufficientFunds -> throw InsufficientFundsException() + is EhError.IpBanned -> throw IpBannedException(error.message) + is EhError.Error -> throw EhException(error.message) + else -> Unit + } + if (e is ParseException || e is SerializationException) { body.onLeft { if ("<" !in it) throw IpBannedException(it) } if (Settings.saveParseErrorBody.value) body.saveParseError(e) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/exception/EhError.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/exception/EhError.kt new file mode 100644 index 000000000..f02ab48e0 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/exception/EhError.kt @@ -0,0 +1,48 @@ +package com.hippo.ehviewer.client.exception + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.CborArray + +@Serializable +sealed interface EhError { + @CborArray + @Serializable + @SerialName("NoHits") + data object NoHits : EhError + + @CborArray + @Serializable + @SerialName("NoWatched") + data object NoWatched : EhError + + @CborArray + @Serializable + @SerialName("NeedLogin") + data object NeedLogin : EhError + + @CborArray + @Serializable + @SerialName("NoHathClient") + data object NoHathClient : EhError + + @CborArray + @Serializable + @SerialName("InsufficientFunds") + data object InsufficientFunds : EhError + + @CborArray + @Serializable + @SerialName("GalleryUnavailable") + data class GalleryUnavailable(val message: String) : EhError + + @CborArray + @Serializable + @SerialName("IpBanned") + data class IpBanned(val message: String) : EhError + + @CborArray + @Serializable + @SerialName("Error") + data class Error(val message: String) : EhError +} diff --git a/app/src/main/rust/src/ffi/jvm.rs b/app/src/main/rust/src/ffi/jvm.rs index 4b8fb45c8..91d912cac 100644 --- a/app/src/main/rust/src/ffi/jvm.rs +++ b/app/src/main/rust/src/ffi/jvm.rs @@ -257,17 +257,28 @@ where { jni_throwing(env, |env| { let buffer = deref_mut_direct_bytebuffer(env, str)?; - let value = { - // SAFETY: ktor client ensure html content is valid utf-8. - let body = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) }; - f(body)? - }; - let mut cursor = Cursor::new(buffer); - serde_cbor::to_writer(&mut cursor, &value)?; - Ok(cursor.position() as i32) + + // SAFETY: ktor client ensure html content is valid utf-8. + let body = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) }; + match f(body) { + Ok(value) => serialize_to_buffer(buffer, &value), + Err(err) => { + if let Some(value) = err.downcast_ref::() { + serialize_to_buffer(buffer, value) + } else { + Err(err) + } + } + } }) } +fn serialize_to_buffer(buffer: &mut [u8], value: &T) -> Result { + let mut cursor = Cursor::new(buffer); + serde_cbor::to_writer(&mut cursor, value)?; + Ok(cursor.position() as i32) +} + #[unsafe(no_mangle)] #[allow(non_snake_case)] pub extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint { diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index 18e62ea01..62559a2db 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -5,6 +5,7 @@ mod ffi; pub mod img; pub mod parser; +use serde::{Serialize, Serializer, ser::SerializeTuple}; use std::fmt::{Debug, Display, Formatter}; use tl::{Bytes, HTMLTag, Node, NodeHandle, Parser, VDom}; @@ -74,22 +75,47 @@ enum EhError { NeedLogin, NoHathClient, InsufficientFunds, + GalleryUnavailable(String), IpBanned(String), Error(String), } +impl EhError { + fn variant_name(&self) -> &'static str { + match self { + EhError::NoHits => "NoHits", + EhError::NoWatched => "NoWatched", + EhError::NeedLogin => "NeedLogin", + EhError::NoHathClient => "NoHathClient", + EhError::InsufficientFunds => "InsufficientFunds", + EhError::GalleryUnavailable(_) => "GalleryUnavailable", + EhError::IpBanned(_) => "IpBanned", + EhError::Error(_) => "Error", + } + } +} + +impl Serialize for EhError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element(self.variant_name())?; + let payload = match self { + EhError::GalleryUnavailable(s) | EhError::IpBanned(s) | EhError::Error(s) => { + std::slice::from_ref(s) + } + _ => &[], + }; + tup.serialize_element(payload)?; + tup.end() + } +} + impl Display for EhError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let msg = match self { - EhError::NoHits => "0", - EhError::NoWatched => "1", - EhError::NeedLogin => "2", - EhError::NoHathClient => "3", - EhError::InsufficientFunds => "4", - EhError::IpBanned(s) => &format!("5{s}"), - EhError::Error(s) => &format!("6{s}"), - }; - f.write_str(msg) + Debug::fmt(&self, f) } } diff --git a/app/src/main/rust/src/parser/detail.rs b/app/src/main/rust/src/parser/detail.rs index 2cc951bbc..4719181b8 100644 --- a/app/src/main/rust/src/parser/detail.rs +++ b/app/src/main/rust/src/parser/detail.rs @@ -470,11 +470,11 @@ pub fn parse_pages(dom: &VDom, parser: &Parser) -> Result { } pub fn parse_preview_list(dom: &VDom, parser: &Parser) -> Result> { - // Check Gallery Not Available let error = get_vdom_first_element_by_class_name(dom, "d"); if let Some(tag) = error.and_then(|n| get_first_child(n.as_tag()?, parser)) { let node = tag.children().top()[0].get(parser).unwrap(); - bail!(EhError::Error(node.inner_text(parser).to_string())); + let err = EhError::GalleryUnavailable(node.inner_text(parser).to_string()); + bail!(err); } dom.get_element_by_id("gdt")