Refine FFI error handling

This commit is contained in:
FooIbar committed 2025-11-21 09:18:11 +08:00
1 parent f7a70cdfc3
commit 3f784ec1de
5 files changed
+129 -37

No files matched your search

@@ -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<String, ByteBuff
throw IpBannedException(message)
}
// Check parse error thrown by rust
val cause = e.cause
if (e is ParseException && cause is RuntimeException) {
when (val message = cause.message) {
"0" -> 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<EhError>(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)
@@ -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
}
+19 -8
View File
@@ -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::<EhError>() {
serialize_to_buffer(buffer, value)
} else {
Err(err)
}
}
}
})
}
fn serialize_to_buffer<T: Serialize>(buffer: &mut [u8], value: &T) -> Result<i32> {
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 {
+36 -10
View File
@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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)
}
}
+2 -2
View File
@@ -470,11 +470,11 @@ pub fn parse_pages(dom: &VDom, parser: &Parser) -> Result<i32> {
}
pub fn parse_preview_list(dom: &VDom, parser: &Parser) -> Result<Vec<GalleryPreview>> {
// 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")