Refactor Rust parser, decouple with JNI for better further KMP support (#568)

* Use serde & kotlinx serialization to marshal jni data

* Introduce unmarshalParsingAs

* Comments

* Introduce parse_marshal_inplace

* Convert parseLimit

* Aha, convert parseGalleryInfoList

* Drop cache_unwind

* Unwind manually

* Fixes

* Drop jnix, update jni

* Data class

* Cleanups

* Cleanups

* Optimize a copy

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-12-30 20:45:11 +00:00
1 parent 5a9ab6c4eb
commit 6e3471ba1b
29 files changed
+308 -1231

No files matched your search

@@ -31,6 +31,7 @@ import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.client.parser.FavParserResult
import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.client.parser.ForumsParser
import com.hippo.ehviewer.client.parser.GalleryApiParser
@@ -324,7 +325,7 @@ object EhEngine {
}
}.fetchUsingAsText(HomeParser::parseResetLimits)
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavoritesParser.Result {
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavParserResult {
val url = ehUrl(EhUrl.FAV_PATH) {
if (FavListUrlBuilder.isValidFavCat(srcCat)) addQueryParameter("favcat", srcCat.toString())
}.buildString()
@@ -16,17 +16,18 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
@Entity(tableName = "GALLERIES")
data class BaseGalleryInfo @Keep constructor(
data class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@@ -0,0 +1,34 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.parseAs
import java.nio.ByteBuffer
import okio.Buffer
import okio.Source
import okio.Timeout
import okio.buffer
fun ByteBuffer.asSource() = object : Source {
private val buffer = this@asSource.slice()
private val len = buffer.capacity()
override fun close() = Unit
override fun read(sink: Buffer, byteCount: Long): Long {
if (buffer.position() == len) return -1
val pos = buffer.position()
val newLimit = (pos + byteCount).toInt().coerceAtMost(len)
buffer.limit(newLimit)
return sink.write(buffer).toLong()
}
override fun timeout() = Timeout.NONE
}
inline fun <reified T> unmarshalParsingAs(body: ByteBuffer, parser: (ByteBuffer, Int) -> Int): T {
// We want to use cbor as it have smaller output size
// But we would rather reuse allocated ByteArrays (i.e. decode with okio Buffer)
val jsonBytes = parser(body, body.limit())
body.clear()
body.limit(jsonBytes)
return body.asSource().buffer().use { it.parseAs<T>() }
}
@@ -3,30 +3,29 @@ package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching
@Serializable
data class FavParserResult(
val catArray: List<String>,
val countArray: List<Int>,
val galleryListResult: GalleryListResult,
) {
val prev = galleryListResult.prev
val next = galleryListResult.next
val galleryInfoList = galleryListResult.galleryInfoList
}
object FavoritesParser {
suspend fun parse(body: ByteBuffer): Result {
val catArray = arrayOfNulls<String>(10)
val countArray = runSuspendCatching {
parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) }
fun parse(body: ByteBuffer): FavParserResult {
return runSuspendCatching {
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
}.getOrElse {
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
throw ParseException("Parse favorites error", it)
}
val result = GalleryListParser.parse(body)
return Result(catArray.requireNoNulls(), countArray, result)
}
class Result(
val catArray: Array<String>,
val countArray: IntArray,
galleryListResult: GalleryListResult,
) {
val prev = galleryListResult.prev
val next = galleryListResult.next
val galleryInfoList = galleryListResult.galleryInfoList
}
}
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit(), favCat: Array<String?>): IntArray
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
@@ -1,6 +1,5 @@
package com.hippo.ehviewer.client.parser
import androidx.annotation.Keep
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.BaseGalleryInfo
@@ -9,11 +8,12 @@ import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching
object GalleryListParser {
suspend fun parse(body: ByteBuffer) = runSuspendCatching {
parseGalleryInfoList(body)
unmarshalParsingAs<GalleryListResult>(body, ::parseGalleryInfoList)
}.onFailure {
if (it is RuntimeException) {
if (it.message?.startsWith("Your IP address") == true) {
@@ -39,10 +39,11 @@ object GalleryListParser {
val emptyResult = GalleryListResult(null, null, arrayListOf())
}
class GalleryListResult @Keep constructor(
@Serializable
data class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): GalleryListResult
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -1,18 +1,18 @@
package com.hippo.ehviewer.client.parser
import android.os.Parcelable
import androidx.annotation.Keep
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
object HomeParser {
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
fun parse(body: ByteBuffer) = runCatching {
parseLimit(body)
unmarshalParsingAs<Limits>(body, ::parseLimit)
}.getOrElse { throw ParseException("Parse image limits error", it) }
fun parseResetLimits(body: String) {
@@ -36,6 +36,7 @@ object HomeParser {
}
@Parcelize
data class Limits @Keep constructor(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
@Serializable
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Int
@@ -1,16 +1,19 @@
package com.hippo.ehviewer.client.parser
import androidx.annotation.Keep
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
object TorrentParser {
fun parse(body: ByteBuffer) = runCatching {
parseTorrent(body)
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
unmarshalParsingAs<TorrentResult>(body, ::parseTorrent)
}.getOrElse {
throw ParseException("Can't parse torrent list", it)
}
}
class Torrent @Keep constructor(
@Serializable
data class Torrent(
val posted: String,
val size: String,
val seeds: Int,
@@ -24,4 +27,4 @@ fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloa
typealias TorrentResult = List<Torrent>
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): Int
@@ -152,8 +152,8 @@ fun FavouritesScreen(navigator: DestinationsNavigator) {
}.onFailure {
return@withIOContext LoadResult.Error(it)
}.getOrThrow()
Settings.favCat = r.catArray
Settings.favCount = r.countArray
Settings.favCat = r.catArray.toTypedArray()
Settings.favCount = r.countArray.toIntArray()
Settings.favCloudCount = r.countArray.sum()
urlBuilder.jumpTo = null
LoadResult.Page(r.galleryInfoList, r.prev, r.next)
+3 -4
View File
@@ -8,10 +8,7 @@ crate_type = ["cdylib"]
[dependencies]
regex-lite = { version = "0.1" }
jnix = { path = "jnix" }
jnix-macros = "0.4"
catch_panic = { path = "catch_panic" }
catch_panic_macros = { path = "catch_panic_macros" }
jni = "0.21"
jni_fn = "0.1.2"
quick-xml = { default-features = false, version = "0.31" }
tl = "0.7"
@@ -19,6 +16,8 @@ log = "0.4"
android_logger = { default-features = false, version = "0.13" }
apply = "0.3"
once_cell = { default-features = false, version = "1.19" }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
[profile.release]
strip = true
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "catch_panic"
version = "1.0.0"
edition = "2021"
authors = [
"Arc-blroth <45273859+Arc-blroth@users.noreply.github.com>",
"Cayden Pocrass <caydenrp9@gmail.com>",
"AdityaJain1030 <50159919+AdityaJain1030@users.noreply.github.com>"
]
readme = "../README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/HermitSocialClub/catch_panic"
description = "A helper macro for safe Java-Rust interop that \"catches\" Rust panics and rethrows them as Java exceptions."
keywords = ["ffi", "jni", "java"]
categories = ["api-bindings"]
[dependencies]
jni = "0.20"
catch_panic_macros = { path = "../catch_panic_macros" }
@@ -1,38 +0,0 @@
//! Implementation of the default `#[catch_panic]` handler.
use std::any::Any;
use std::panic::{catch_unwind, UnwindSafe};
use jni::JNIEnv;
/// The wrapper method that `#[catch_panic]` invokes to actually catch panics.
/// This is an internal implementation detail - don't call this function directly!
#[doc(hidden)]
#[inline]
pub fn __catch_panic<F, R, H>(env: JNIEnv, default: R, handler: H, f: F) -> R
where
F: FnOnce() -> R + UnwindSafe,
H: FnOnce(JNIEnv, Box<dyn Any + Send + 'static>),
{
match catch_unwind(f) {
Ok(result) => result,
Err(err) => {
handler(env, err);
default
}
}
}
/// `#[catch_panic]`'s default panic handler. This
/// will rethrow all caught panics as java `RuntimeException`s
/// with the message passed to `panic!`.
pub fn default_handler(env: JNIEnv, err: Box<dyn Any + Send + 'static>) {
let msg = match err.downcast_ref::<&'static str>() {
Some(s) => *s,
None => match err.downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<dyn Any>",
},
};
env.throw_new("java/lang/RuntimeException", msg).unwrap();
}
-102
View File
@@ -1,102 +0,0 @@
//! # #\[catch_panic\]
//!
//! A helper macro for safe Java-Rust interop that "catches" Rust panics
//! and rethrows them as Java exceptions.
//!
//! ## Usage
//!
//! Attach `#[catch_panic]` to a JNI callback to have panics converted into `RuntimeException`s:
//!
//! ```
//! # use jni::JNIEnv;
//! # use catch_panic::catch_panic;
//! #
//! #[no_mangle]
//! #[catch_panic]
//! pub extern "C" fn Java_com_example_Example_panic(_env: JNIEnv) {
//! panic!("everything is not fine");
//! }
//!
//! # catch_panic::test::check_callback(|env| Java_com_example_Example_panic(env), true);
//! ```
//!
//! Since `#[catch_panic]` internally relies on Rust's [catch_unwind] function,
//! you **must** build your library with [`panic = "unwind"`] for panics to be caught.
//!
//! [catch_unwind]: std::panic::catch_unwind
//! [`panic = "unwind"`]: https://doc.rust-lang.org/cargo/reference/profiles.html#panic
//!
//! ### Dummy return values
//!
//! Even if an exception is thrown, the native callback must return some value to the JVM.
//! By default, `#[catch_panic]` will attempt to use the [Default][std::default::Default]
//! value of a type as the dummy value to return. This however does not work for `jobject`s
//! and related types, so an explicit default must be specified:
//!
//! ```
//! # use jni::JNIEnv;
//! # use jni::sys::jobject;
//! # use catch_panic::catch_panic;
//! #
//! #[no_mangle]
//! #[catch_panic(default = "std::ptr::null_mut()")]
//! pub extern "C" fn Java_com_example_Example_gimmeAnObject(mut env: JNIEnv) -> jobject {
//! env.alloc_object("java/lang/Object").unwrap().into_inner()
//! }
//!
//! # catch_panic::test::check_callback(|env| Java_com_example_Example_gimmeAnObject(env), false);
//! ```
//!
//! Any valid expression can be used for the default value.
//!
//! ### Custom panic payload handlers
//!
//! For throwing custom exception types or processing the panic payload,
//! you can use your own panic payload handler:
//!
//! ```
//! # use std::any::Any;
//! # use jni::JNIEnv;
//! # use catch_panic::catch_panic;
//! #
//! pub fn enterprise_certified_handler(mut env: JNIEnv, err: Box<dyn Any + Send + 'static>) {
//! let msg = match err.downcast_ref::<&'static str>() {
//! Some(s) => *s,
//! None => match err.downcast_ref::<String>() {
//! Some(s) => &s[..],
//! None => "this is a certified `std::panic::panic_any` moment",
//! },
//! };
//! env.throw_new("com/example/ExampleEnterpriseException", msg).unwrap();
//! }
//!
//! #[no_mangle]
//! #[catch_panic(handler = "enterprise_certified_handler")]
//! pub extern "C" fn Java_com_example_Example_makeFactoryNoises(env: JNIEnv) {
//! panic!("<insert factory noises>");
//! }
//!
//! # catch_panic::test::check_callback_with_setup(
//! # |env| {
//! # let src = std::fs::read("testlib/ExampleEnterpriseException.class")
//! # .expect("ExampleEnterpriseException test class file missing!");
//! #
//! # let object_class = env.find_class("java/lang/Object").unwrap();
//! # let object_class_loader = env
//! # .call_method(object_class, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])
//! # .unwrap();
//! #
//! # env.define_class(
//! # "com/example/ExampleEnterpriseException",
//! # object_class_loader.try_into().unwrap(),
//! # &src,
//! # )
//! # .unwrap();
//! # },
//! # |env| Java_com_example_Example_makeFactoryNoises(env),
//! # true,
//! # );
//! ```
pub mod handler;
pub use catch_panic_macros::catch_panic;
@@ -1,22 +0,0 @@
[package]
name = "catch_panic_macros"
version = "1.0.0"
edition = "2021"
authors = [
"Arc-blroth <45273859+Arc-blroth@users.noreply.github.com>",
"Cayden Pocrass <caydenrp9@gmail.com>",
"AdityaJain1030 <50159919+AdityaJain1030@users.noreply.github.com>"
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/HermitSocialClub/catch_panic"
description = "The proc macro impl for catch_panic"
keywords = ["ffi", "jni", "java"]
categories = ["api-bindings"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
@@ -1,108 +0,0 @@
use std::ops::Deref;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Path};
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
const FIRST_ARG_MSG: &str =
"#[catch_panic] requires that this function has at least one argument of type jni::JNIEnv";
const INVALID_NAME_MSG: &str = "Invalid function parameter name";
/// Aborts parsing with the given error.
/// This will also emit the original item as-is to suppress red herring unused warnings.
macro_rules! abort {
($err:expr, $item:expr $(,)?) => {{
let err = $err;
let item = $item;
return TokenStream::from(quote! {
#err
#item
});
}};
}
/// Catches a panic and rethrows it as a Java exception.
/// See the [crate-level documentation](../catch_panic/index.html) for more information.
#[proc_macro_attribute]
pub fn catch_panic(attr: TokenStream, item: TokenStream) -> TokenStream {
let item = proc_macro2::TokenStream::from(item);
// parse macro arguments
let mut default_value = None;
let mut handler = None;
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("default") {
default_value = Some(meta.value()?.parse::<LitStr>()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("handler") {
handler = Some(meta.value()?.parse::<LitStr>()?.parse::<Path>()?);
Ok(())
} else {
Err(meta.error("unsupported argument"))
}
});
parse_macro_input!(attr with parser);
let default_value = match default_value {
Some(val) => val.to_token_stream(),
None => quote! { ::std::default::Default::default() },
};
let handler = match handler {
Some(val) => val.to_token_stream(),
None => quote! { ::catch_panic::handler::default_handler },
};
// parse and validate function
let item_span = item.span();
let (attrs, vis, sig, block) = match syn::parse2::<ItemFn>(item.clone()) {
Ok(ItemFn {
attrs,
vis,
sig,
block,
}) => (attrs, vis, sig, block),
Err(_) => abort!(
quote_spanned! {item_span=>
compile_error!(#ONLY_FUNCTIONS_MSG);
},
item,
),
};
if sig.inputs.is_empty() {
abort!(
quote_spanned! {sig.span()=>
compile_error!(#FIRST_ARG_MSG);
},
item,
);
}
let first_arg_name = match sig.inputs.first().unwrap() {
FnArg::Receiver(receiver) => abort!(
quote_spanned! {receiver.span()=>
compile_error!(#FIRST_ARG_MSG);
},
item,
),
FnArg::Typed(pat_type) => match pat_type.pat.deref() {
Pat::Ident(pat) => pat.ident.clone(),
_ => abort!(
quote_spanned! {pat_type.span()=>
compile_error!(#INVALID_NAME_MSG);
},
item,
),
},
};
TokenStream::from(quote! {
#(#attrs)*
#vis #sig {
::catch_panic::handler::__catch_panic(#first_arg_name, #default_value, #handler, move || {
#block
})
}
})
}
-23
View File
@@ -1,23 +0,0 @@
[package]
name = "jnix"
description = "High-level extensions to help with the usage of JNI in Rust code"
version = "0.5.0"
authors = ["Mullvad VPN"]
readme = "README.md"
license = "Apache-2.0 OR MIT"
keywords = ["ffi", "java", "jni"]
categories = ["external-ffi-bindings"]
repository = "https://github.com/mullvad/jnix"
edition = "2018"
[features]
derive = ["jnix-macros"]
[dependencies]
jni = "0.20"
jnix-macros = { version = "0.4", optional = true }
once_cell = "1.19"
parking_lot = "0.12"
[dev-dependencies]
jnix-macros = { version = "0.4" }
-39
View File
@@ -1,39 +0,0 @@
use jni::objects::{AutoLocal, JValue};
/// Returns a value as its [`JValue`] representation.
///
/// [`JValue`]: JValue
pub trait AsJValue<'env> {
/// Returns the [`JValue`] representation of the type.
///
/// [`JValue`]: JValue
fn as_jvalue<'borrow>(&'borrow self) -> JValue<'borrow>
where
'env: 'borrow;
}
impl<'env_borrow, 'env: 'env_borrow> AsJValue<'env> for AutoLocal<'env, 'env_borrow> {
fn as_jvalue<'borrow>(&'borrow self) -> JValue<'borrow>
where
'env: 'borrow,
{
JValue::Object(self.as_obj())
}
}
macro_rules! impl_for_primitives {
( $( $primitive:ty ),* $(,)* ) => {
$(
impl<'env> AsJValue<'env> for $primitive {
fn as_jvalue<'borrow>(&'borrow self) -> JValue<'borrow>
where
'env: 'borrow,
{
JValue::from(*self)
}
}
)*
};
}
impl_for_primitives!((), bool, u8, i8, u16, i16, i32, i64, f32, f64);
@@ -1 +0,0 @@
mod std;
@@ -1,210 +0,0 @@
use crate::{FromJava, JnixEnv};
use jni::signature::ReturnType;
use jni::{
objects::{AutoLocal, JObject, JString, JValue},
signature::Primitive,
sys::{jboolean, jint, JNI_FALSE},
};
impl<'env, 'sub_env, T> FromJava<'env, JValue<'sub_env>> for T
where
'env: 'sub_env,
T: FromJava<'env, JObject<'sub_env>>,
{
const JNI_SIGNATURE: &'static str = T::JNI_SIGNATURE;
fn from_java(env: &JnixEnv<'env>, source: JValue<'sub_env>) -> Self {
match source {
JValue::Object(object) => T::from_java(env, object),
_ => panic!(
"Can't convert non-object Java type. Expected type signature {}",
Self::JNI_SIGNATURE
),
}
}
}
impl<'env, 'sub_env, 'borrow, T> FromJava<'env, AutoLocal<'sub_env, 'borrow>> for T
where
'env: 'sub_env,
'sub_env: 'borrow,
T: for<'inner_borrow> FromJava<'env, JObject<'inner_borrow>>,
{
const JNI_SIGNATURE: &'static str = T::JNI_SIGNATURE;
fn from_java(env: &JnixEnv<'env>, source: AutoLocal<'sub_env, 'borrow>) -> Self {
T::from_java(env, source.as_obj())
}
}
impl<'env> FromJava<'env, jboolean> for bool {
const JNI_SIGNATURE: &'static str = "Z";
fn from_java(_: &JnixEnv<'env>, source: jboolean) -> Self {
source != JNI_FALSE
}
}
impl<'env, 'sub_env> FromJava<'env, JValue<'sub_env>> for bool
where
'env: 'sub_env,
{
const JNI_SIGNATURE: &'static str = "Z";
fn from_java(env: &JnixEnv<'env>, source: JValue<'sub_env>) -> Self {
match source {
JValue::Bool(boolean) => bool::from_java(env, boolean),
_ => panic!("Can't convert Java type, expected a boolean primitive"),
}
}
}
impl<'env> FromJava<'env, jint> for i32 {
const JNI_SIGNATURE: &'static str = "I";
fn from_java(_: &JnixEnv<'env>, source: jint) -> Self {
source as i32
}
}
impl<'env, 'sub_env> FromJava<'env, JValue<'sub_env>> for i32
where
'env: 'sub_env,
{
const JNI_SIGNATURE: &'static str = "I";
fn from_java(env: &JnixEnv<'env>, source: JValue<'sub_env>) -> Self {
match source {
JValue::Int(integer) => i32::from_java(env, integer),
_ => panic!("Can't convert Java type, expected an integer primitive"),
}
}
}
impl<'env, 'sub_env> FromJava<'env, JString<'sub_env>> for String
where
'env: 'sub_env,
{
const JNI_SIGNATURE: &'static str = "Ljava/lang/String;";
fn from_java(env: &JnixEnv<'env>, source: JString<'sub_env>) -> Self {
String::from(
env.get_string(source)
.expect("Failed to convert from Java String"),
)
}
}
impl<'env, 'sub_env> FromJava<'env, JObject<'sub_env>> for String
where
'env: 'sub_env,
{
const JNI_SIGNATURE: &'static str = "Ljava/lang/String;";
fn from_java(env: &JnixEnv<'env>, source: JObject<'sub_env>) -> Self {
String::from_java(env, JString::from(source))
}
}
impl<'env, 'sub_env, T> FromJava<'env, JObject<'sub_env>> for Option<T>
where
'env: 'sub_env,
T: FromJava<'env, JObject<'sub_env>>,
{
const JNI_SIGNATURE: &'static str = T::JNI_SIGNATURE;
fn from_java(env: &JnixEnv<'env>, source: JObject<'sub_env>) -> Self {
if source.is_null() {
None
} else {
Some(T::from_java(env, source))
}
}
}
impl<'env, 'sub_env, T> FromJava<'env, JString<'sub_env>> for Option<T>
where
'env: 'sub_env,
T: FromJava<'env, JString<'sub_env>>,
{
const JNI_SIGNATURE: &'static str = T::JNI_SIGNATURE;
fn from_java(env: &JnixEnv<'env>, source: JString<'sub_env>) -> Self {
if source.is_null() {
None
} else {
Some(T::from_java(env, source))
}
}
}
impl<'env, 'sub_env> FromJava<'env, JObject<'sub_env>> for Option<i32> {
const JNI_SIGNATURE: &'static str = "Ljava/lang/Integer;";
fn from_java(env: &JnixEnv<'env>, source: JObject<'sub_env>) -> Self {
if source.is_null() {
None
} else {
let class = env.get_class("java/lang/Integer");
let method_id = env
.get_method_id(&class, "intValue", "()I")
.expect("Failed to get method ID for Integer.intValue()");
let return_type = ReturnType::Primitive(Primitive::Int);
let int_value = env
.call_method_unchecked(source, method_id, return_type, &[])
.expect("Failed to call Integer.intValue()")
.i()
.expect("Call to Integer.intValue() did not return an int primitive");
Some(int_value)
}
}
}
impl<'env, 'sub_env, T> FromJava<'env, JObject<'sub_env>> for Vec<T>
where
'env: 'sub_env,
T: FromJava<'env, JObject<'sub_env>>,
{
const JNI_SIGNATURE: &'static str = "Ljava/util/ArrayList;";
fn from_java(env: &JnixEnv<'env>, source: JObject<'sub_env>) -> Self {
let class = env.get_class("java/util/ArrayList");
let size_method_id = env
.get_method_id(&class, "size", "()I")
.expect("Failed to get method ID for ArrayList.size()");
let size_return_type = ReturnType::Primitive(Primitive::Int);
let item_count = env
.call_method_unchecked(source, size_method_id, size_return_type, &[])
.expect("Failed to call ArrayList.size()")
.i()
.expect("Call to ArrayList.size() did not return an int primitive");
let mut target = Vec::with_capacity(item_count as usize);
let get_method_id = env
.get_method_id(&class, "get", "(I)Ljava/lang/Object;")
.expect("Failed to get method ID for ArrayList.get()");
let get_return_type = ReturnType::Object;
for index in 0..item_count {
let object = env
.call_method_unchecked(
source,
get_method_id,
get_return_type.clone(),
&[JValue::from(index).to_jni()],
)
.expect("Failed to call ArrayList.get()")
.l()
.expect("Call to ArrayList.get() did not return an object");
let item = T::from_java(env, object);
target.push(item);
}
target
}
}
@@ -1,16 +0,0 @@
mod implementations;
use crate::JnixEnv;
/// Conversion from a Java type into its Rust equivalent.
///
/// A type that implements this trait has an equivalent Java representation.
pub trait FromJava<'env, JavaType> {
/// The [JNI signature] of the Java type.
///
/// [JNI signature]: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp16432
const JNI_SIGNATURE: &'static str;
/// Performs the conversion.
fn from_java(env: &JnixEnv<'env>, source: JavaType) -> Self;
}
@@ -1 +0,0 @@
mod std;
@@ -1,224 +0,0 @@
use crate::{IntoJava, JnixEnv};
use jni::sys::jfloat;
use jni::{
objects::{AutoLocal, JList, JObject, JValue},
sys::{jboolean, jdouble, jint, jlong, jshort, jsize, JNI_FALSE, JNI_TRUE},
};
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for bool {
const JNI_SIGNATURE: &'static str = "Z";
type JavaType = jboolean;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
if self {
JNI_TRUE
} else {
JNI_FALSE
}
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for i16 {
const JNI_SIGNATURE: &'static str = "S";
type JavaType = jshort;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
self as jshort
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for i32 {
const JNI_SIGNATURE: &'static str = "I";
type JavaType = jint;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
self as jint
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for i64 {
const JNI_SIGNATURE: &'static str = "J";
type JavaType = jlong;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
self as jlong
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for f32 {
const JNI_SIGNATURE: &'static str = "F";
type JavaType = jfloat;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
self as jfloat
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for f64 {
const JNI_SIGNATURE: &'static str = "D";
type JavaType = jdouble;
fn into_java(self, _: &'borrow JnixEnv<'env>) -> Self::JavaType {
self as jdouble
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for &'_ [u8] {
const JNI_SIGNATURE: &'static str = "[B";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
let size = self.len();
let array = env
.new_byte_array(size as jsize)
.expect("Failed to create a Java array of bytes");
let data = unsafe { std::slice::from_raw_parts(self.as_ptr() as *const i8, size) };
env.set_byte_array_region(array, 0, data)
.expect("Failed to copy bytes to Java array");
env.auto_local(unsafe { JObject::from_raw(array) })
}
}
macro_rules! impl_into_java_for_array {
($element_type:ty) => {
impl_into_java_for_array!(
$element_type,
0 1 2 3 4 5 6 7
8 9 10 11 12 13 14 15
16 17 18 19 20 21 22 23
24 25 26 27 28 29 30 31
32
);
};
($element_type:ty, $( $count:tt )*) => {
$(
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for [$element_type; $count] {
const JNI_SIGNATURE: &'static str = "[B";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
(&self as &[$element_type]).into_java(env)
}
}
)*
};
}
impl_into_java_for_array!(u8);
impl<'borrow, 'env, T> IntoJava<'borrow, 'env> for Option<T>
where
'env: 'borrow,
T: IntoJava<'borrow, 'env, JavaType = AutoLocal<'env, 'borrow>>,
{
const JNI_SIGNATURE: &'static str = T::JNI_SIGNATURE;
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
match self {
Some(t) => t.into_java(env),
None => env.auto_local(JObject::null()),
}
}
}
impl<'borrow, 'env> IntoJava<'borrow, 'env> for Option<bool>
where
'env: 'borrow,
{
const JNI_SIGNATURE: &'static str = "Ljava/lang/Boolean;";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
match self {
Some(value) => {
let class = env.get_class("java/lang/Boolean");
let boxed_boolean = env
.new_object(&class, "(Z)V", &[JValue::Bool(value as jboolean)])
.expect("Failed to create boxed Boolean object");
env.auto_local(boxed_boolean)
}
None => env.auto_local(JObject::null()),
}
}
}
impl<'borrow, 'env> IntoJava<'borrow, 'env> for Option<i32>
where
'env: 'borrow,
{
const JNI_SIGNATURE: &'static str = "Ljava/lang/Integer;";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
match self {
Some(value) => {
let class = env.get_class("java/lang/Integer");
let boxed_boolean = env
.new_object(&class, "(I)V", &[JValue::Int(value as jint)])
.expect("Failed to create boxed Integer object");
env.auto_local(boxed_boolean)
}
None => env.auto_local(JObject::null()),
}
}
}
impl<'borrow, 'env, T> IntoJava<'borrow, 'env> for Vec<T>
where
'env: 'borrow,
T: IntoJava<'borrow, 'env, JavaType = AutoLocal<'env, 'borrow>>,
{
const JNI_SIGNATURE: &'static str = "Ljava/util/ArrayList;";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
let initial_capacity = self.len();
let parameters = [JValue::Int(initial_capacity as jint)];
let class = env.get_class("java/util/ArrayList");
let list_object = env
.new_object(&class, "(I)V", &parameters)
.expect("Failed to create ArrayList object");
let list =
JList::from_env(env, list_object).expect("Failed to create JList from ArrayList");
for element in self {
list.add(element.into_java(env).as_obj())
.expect("Failed to add element to ArrayList");
}
env.auto_local(list_object)
}
}
impl<'borrow, 'env: 'borrow> IntoJava<'borrow, 'env> for String {
const JNI_SIGNATURE: &'static str = "Ljava/lang/String;";
type JavaType = AutoLocal<'env, 'borrow>;
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType {
let jstring = env.new_string(&self).expect("Failed to create Java String");
env.auto_local(jstring)
}
}
@@ -1,27 +0,0 @@
mod implementations;
use crate::{AsJValue, JnixEnv};
/// Conversion from a type into its Java representation.
///
/// A type that implements this trait has an equivalent Java representation.
pub trait IntoJava<'borrow, 'env: 'borrow> {
/// The [JNI signature] of the Java type.
///
/// [JNI signature]: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp16432
const JNI_SIGNATURE: &'static str;
/// The Rust type that wraps a reference to the Java type.
type JavaType: AsJValue<'env>;
/// Performs the conversion.
fn into_java(self, env: &'borrow JnixEnv<'env>) -> Self::JavaType;
/// Returns the JNI signature of the Java type.
///
/// This is a helper method to obtain the `JNI_SIGNATURE` from a value without knowing its
/// exact type name.
fn jni_signature(&self) -> &'static str {
Self::JNI_SIGNATURE
}
}
-81
View File
@@ -1,81 +0,0 @@
use jni::{
objects::{GlobalRef, JObject},
JNIEnv,
};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::{borrow::Cow, collections::HashMap, ops::Deref};
static CLASS_CACHE: Lazy<Mutex<HashMap<String, GlobalRef>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
/// Provides access to JNI functions.
///
/// Also provides an internal cache for loaded classes.
pub struct JnixEnv<'env> {
pub env: JNIEnv<'env>,
}
impl<'env> From<JNIEnv<'env>> for JnixEnv<'env> {
fn from(env: JNIEnv<'env>) -> Self {
JnixEnv { env }
}
}
impl<'env> Deref for JnixEnv<'env> {
type Target = JNIEnv<'env>;
fn deref(&self) -> &Self::Target {
&self.env
}
}
impl<'env> JnixEnv<'env> {
/// Retrieves a global reference to a Class object.
///
/// Tries to retrieve it from the class cache. If it's not present in the cache, it is loaded
/// into the cache first.
///
/// # Panics
///
/// This method will panic if the class can't be loaded or if a global reference can't be
/// obtained to the Class object.
pub fn get_class<'a>(&self, class_name: impl Into<Cow<'a, str>>) -> GlobalRef {
let class_name = class_name.into();
let mut cache = CLASS_CACHE.lock();
if let Some(class) = cache.get(class_name.as_ref()) {
class.clone()
} else {
let class = self.load_class(class_name.as_ref());
cache.insert(class_name.into_owned(), class.clone());
class
}
}
/// Loads a class and stores it in the class cache.
pub fn preload_classes(&self, class_names: impl IntoIterator<Item = impl Into<String>>) {
let mut cache = CLASS_CACHE.lock();
for class_name in class_names {
let class_name = class_name.into();
let class = self.load_class(&class_name);
cache.insert(class_name, class);
}
}
fn load_class(&self, class_name: impl AsRef<str>) -> GlobalRef {
let class_name = class_name.as_ref();
let local_ref = self
.env
.find_class(class_name)
.expect(&format!("Failed to find {} Java class", class_name));
self.env.new_global_ref(JObject::from(local_ref)).expect(
"Failed to convert local reference to Java class object into a global reference",
)
}
}
-95
View File
@@ -1,95 +0,0 @@
//! This crate provides high-level extensions to help with the usage of [JNI] in Rust code. Internally,
//! it uses the [`jni-rs`] crate for the low-level JNI operations.
//!
//! Some helper traits are provided, such as:
//!
//! - [`AsJValue`]: for allowing a JNI type to be convected to a `JValue` wrapper type.
//! - [`IntoJava`]: for allowing a Rust type to be converted to a Java type.
//! - [`FromJava`]: for allowing a Rust type to be created from a Java type.
//!
//! A [`JnixEnv`] helper type is also provided, which is a [`JNIEnv`] wrapper that contains an
//! internal class cache for preloaded classes.
//!
//! If compiled with the `derive` feature flag, the crate also exports procedural macros to
//! [derive `IntoJava`] and to [derive `FromJava`], which makes writing conversion code a lot
//! easier. An example would be:
//!
//! ```rust
//! use jnix::{
//! jni::{objects::JObject, JNIEnv},
//! JnixEnv, FromJava, IntoJava,
//! };
//!
//! // Rust type definition
//! #[derive(Default, FromJava, IntoJava)]
//! #[jnix(package = "my.package")]
//! pub struct MyData {
//! number: i32,
//! string: String,
//! }
//!
//! // A JNI function called from Java that creates a `MyData` Rust type, converts it to a Java
//! // type and returns it.
//! #[no_mangle]
//! #[allow(non_snake_case)]
//! pub extern "system" fn Java_my_package_JniClass_getData<'env>(
//! env: JNIEnv<'env>,
//! _this: JObject<'env>,
//! data: JObject<'env>,
//! ) -> JObject<'env> {
//! // Create the `JnixEnv` wrapper
//! let env = JnixEnv::from(env);
//!
//! // Convert parameter to Rust type
//! let data = MyData::from_java(&env, data);
//!
//! // Create a new `MyData` object by converting from the Rust type. Since a smart pointer is
//! // returned from `into_java`, the inner object must be "leaked" sothat the garbage collector
//! // can own it afterwards
//! data.into_java(&env).forget()
//! }
//! ```
//!
//! ```java
//! package my.package;
//!
//! public class MyData {
//! public MyData(int number, String string) {
//! // This is the constructor that is called by the generated `IntoJava` code
//! //
//! // Note that the fields don't actually have to exist, the only thing that's necessary
//! // is for the target Java class to have a constructor with the expected type signature
//! // following the field order of the Rust type.
//! }
//!
//! // These getters are called by the generated `FromJava` code
//! public int getNumber() {
//! return 10;
//! }
//!
//! public String getString() {
//! return "string value";
//! }
//! }
//! ```
//!
//! [JNI]: https://en.wikipedia.org./wiki/Java_Native_Interface
//! [`jni-rs`]: https://crates.io/crates/jni
//! [`JNIEnv`]: jni::JNIEnv
//! [`AsJValue`]: AsJValue
//! [`IntoJava`]: IntoJava
//! [`FromJava`]: FromJava
//! [`JnixEnv`]: JnixEnv
//! [derive `IntoJava`]: ../jnix_macros/derive.IntoJava.html
//! [derive `FromJava`]: ../jnix_macros/derive.FromJava.html
pub extern crate jni;
mod as_jvalue;
mod from_java;
mod into_java;
mod jnix_env;
pub use self::{as_jvalue::AsJValue, from_java::FromJava, into_java::IntoJava, jnix_env::JnixEnv};
#[cfg(feature = "derive")]
pub use jnix_macros::{FromJava, IntoJava};
+62 -12
View File
@@ -2,22 +2,24 @@ mod parser;
extern crate android_logger;
extern crate apply;
extern crate catch_panic;
extern crate jni;
extern crate jni_fn;
extern crate jnix;
extern crate jnix_macros;
extern crate log;
extern crate once_cell;
extern crate quick_xml;
extern crate regex_lite;
extern crate serde;
extern crate tl;
use android_logger::Config;
use jnix::jni::objects::JByteBuffer;
use jnix::jni::sys::{jint, JavaVM, JNI_VERSION_1_6};
use jnix::JnixEnv;
use jni::objects::JByteBuffer;
use jni::sys::{jint, JavaVM, JNI_VERSION_1_6};
use jni::JNIEnv;
use log::LevelFilter;
use serde::Serialize;
use std::ffi::c_void;
use std::io::Write;
use std::panic::{catch_unwind, UnwindSafe};
use std::ptr::slice_from_raw_parts;
use std::str::from_utf8_unchecked;
use tl::{Bytes, Node, NodeHandle, Parser, VDom};
@@ -33,6 +35,28 @@ macro_rules! regex {
const EHGT_PREFIX: &str = "https://ehgt.org/";
const EX_PREFIX: &str = "https://s.exhentai.org/";
struct PtrCursor {
ptr: *mut u8,
pos: i32,
}
impl Write for PtrCursor {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let len = buf.len();
unsafe {
self.ptr.copy_from(buf.as_ptr(), len);
self.ptr = self.ptr.add(len);
self.pos += len as i32;
}
Ok(len)
}
fn flush(&mut self) -> std::io::Result<()> {
// Nothing
Ok(())
}
}
fn check_html(str: &str) {
if !str.contains('<') {
panic!("{}", str)
@@ -68,18 +92,44 @@ where
handle.get(parser)
}
fn parse_bytebuffer<F, R>(env: &mut JnixEnv, str: JByteBuffer, limit: jint, mut f: F) -> Option<R>
fn parse_marshal_inplace<F, R>(env: &mut JNIEnv, str: JByteBuffer, limit: jint, mut f: F) -> i32
where
F: FnMut(&VDom, &Parser, &JnixEnv, &str) -> Option<R>,
F: FnMut(&VDom, &Parser, &str) -> Option<R> + UnwindSafe,
R: Serialize,
{
let ptr = env.get_direct_buffer_address(str).ok()?;
let ptr = env.get_direct_buffer_address(&str).unwrap();
let html = unsafe {
let buff = slice_from_raw_parts(ptr, limit as usize);
from_utf8_unchecked(&*buff)
};
let dom = tl::parse(html, tl::ParserOptions::default()).ok()?;
let parser = dom.parser();
f(&dom, parser, env, html)
let f = move || {
let dom = tl::parse(html, tl::ParserOptions::default()).unwrap();
let parser = dom.parser();
let result = f(&dom, parser, html);
match result {
// Nothing to marshal
None => 0,
Some(value) => {
let mut cursor = PtrCursor { ptr, pos: 0 };
serde_json::to_writer(&mut cursor, &value).unwrap();
cursor.pos
}
}
};
match catch_unwind(f) {
Ok(result) => result,
Err(err) => {
let msg = match err.downcast_ref::<&'static str>() {
Some(s) => *s,
None => match err.downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<dyn Any>",
},
};
env.throw_new("java/lang/RuntimeException", msg).unwrap();
0
}
}
}
fn get_node_handle_attr<'a>(
+55 -49
View File
@@ -1,57 +1,63 @@
use apply::Also;
use catch_panic::catch_panic;
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use jnix::jni::objects::{JByteBuffer, JClass};
use jnix::jni::sys::{jint, jintArray, jobjectArray};
use jnix::jni::JNIEnv;
use jnix::JnixEnv;
use parse_bytebuffer;
use parse_marshal_inplace;
use parser::list::{parse_info_list, GalleryListResult};
use quick_xml::escape::unescape;
use serde::Serialize;
use tl::Parser;
use tl::VDom;
#[derive(Serialize)]
#[allow(non_snake_case)]
struct FavResult {
catArray: Vec<String>,
countArray: Vec<i32>,
galleryListResult: GalleryListResult,
}
fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Option<FavResult> {
if html.contains("This page requires you to log on.</p>") {
panic!("Not logged in!")
}
let vec: Vec<(String, i32)> = dom
.get_elements_by_class_name("fp")
.enumerate()
.filter_map(|(i, e)| {
if i == 10 {
return None;
}
let top = e.get(parser)?.children()?;
let children = top.top();
let cat = children[5].get(parser)?.inner_text(parser);
let name = unescape(&cat).ok()?.trim().to_string();
let count = children[1]
.get(parser)?
.inner_text(parser)
.parse::<i32>()
.ok()?;
Some((name, count))
})
.collect();
if vec.len() == 10 {
let list = parse_info_list(dom, parser, html)?;
let cat = vec.iter().cloned().unzip();
Some(FavResult {
catArray: cat.0,
countArray: cat.1,
galleryListResult: list,
})
} else {
None
}
}
#[no_mangle]
#[catch_panic(default = "std::ptr::null_mut()")]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.FavoritesParserKt")]
pub fn parseFav(
env: JNIEnv,
_class: JClass,
input: JByteBuffer,
limit: jint,
str: jobjectArray,
) -> jintArray {
let mut env = JnixEnv { env };
let vec = parse_bytebuffer(&mut env, input, limit, |dom, parser, env, html| {
if html.contains("This page requires you to log on.</p>") {
panic!("Not logged in!")
}
let vec: Vec<i32> = dom
.get_elements_by_class_name("fp")
.enumerate()
.filter_map(|(i, e)| {
if i == 10 {
return None;
}
let top = e.get(parser)?.children()?;
let children = top.top();
let cat = children[5].get(parser)?.inner_text(parser);
let name = unescape(&cat).ok()?;
env.set_object_array_element(str, i as i32, env.new_string(name.trim()).ok()?)
.ok()?;
children[1]
.get(parser)?
.inner_text(parser)
.parse::<i32>()
.ok()
})
.collect();
if vec.len() == 10 {
Some(vec)
} else {
None
}
pub fn parseFav(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, parser, html| {
parse_fav(dom, parser, html)
})
.unwrap();
env.new_int_array(10)
.unwrap()
.also(|it| env.set_int_array_region(*it, 0, &vec).unwrap())
}
+27 -29
View File
@@ -1,42 +1,40 @@
use catch_panic::catch_panic;
use get_vdom_first_element_by_class_name;
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use jnix::jni::objects::{JByteBuffer, JClass};
use jnix::jni::sys::{jint, jobject};
use jnix::jni::JNIEnv;
use jnix::{IntoJava, JnixEnv};
use jnix_macros::IntoJava;
use {get_vdom_first_element_by_class_name, parse_bytebuffer};
use parse_marshal_inplace;
use serde::Serialize;
use tl::Parser;
use tl::VDom;
#[derive(Default, IntoJava)]
#[allow(non_snake_case)]
#[jnix(package = "com.hippo.ehviewer.client.parser")]
pub struct Limits {
#[derive(Serialize)]
struct Limits {
current: i32,
maximum: i32,
resetCost: i32,
}
fn parse_limit(dom: &VDom, parser: &Parser) -> Option<Limits> {
let iter = get_vdom_first_element_by_class_name(dom, "homebox")?
.as_tag()?
.query_selector(parser, "strong")?;
let vec: Vec<i32> = iter
.filter_map(|e| e.get(parser)?.inner_text(parser).parse::<i32>().ok())
.collect();
Some(Limits {
current: vec[0],
maximum: vec[1],
resetCost: vec[2],
})
}
#[no_mangle]
#[catch_panic(default = "std::ptr::null_mut()")]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.HomeParserKt")]
pub fn parseLimit(env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jobject {
let mut env = JnixEnv { env };
parse_bytebuffer(&mut env, input, limit, |dom, parser, _, _| {
let iter = get_vdom_first_element_by_class_name(dom, "homebox")?
.as_tag()?
.query_selector(parser, "strong")?;
let vec: Vec<i32> = iter
.filter_map(|e| e.get(parser)?.inner_text(parser).parse::<i32>().ok())
.collect();
Some(Limits {
current: vec[0],
maximum: vec[1],
resetCost: vec[2],
})
pub fn parseLimit(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, parser, _| {
parse_limit(dom, parser)
})
.unwrap()
.into_java(&env)
.forget()
.into_raw()
}
+52 -62
View File
@@ -1,24 +1,22 @@
use catch_panic::catch_panic;
use check_html;
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use jnix::jni::objects::{JByteBuffer, JClass};
use jnix::jni::sys::{jint, jobject};
use jnix::jni::JNIEnv;
use jnix::{IntoJava, JnixEnv};
use jnix_macros::IntoJava;
use parse_marshal_inplace;
use quick_xml::escape::unescape;
use serde::Serialize;
use std::borrow::Cow;
use std::ops::Index;
use tl::{Node, Parser};
use {check_html, parse_bytebuffer};
use tl::{Node, Parser, VDom};
use {get_element_by_id, get_vdom_first_element_by_class_name};
use {get_first_element_by_class_name, query_childs_first_match_attr};
use {get_node_attr, get_node_handle_attr, regex};
use {EHGT_PREFIX, EX_PREFIX};
#[derive(Default, IntoJava)]
#[derive(Serialize)]
#[allow(non_snake_case)]
#[jnix(package = "com.hippo.ehviewer.client.data")]
pub struct BaseGalleryInfo {
struct BaseGalleryInfo {
gid: i64,
token: String,
title: String,
@@ -40,9 +38,8 @@ pub struct BaseGalleryInfo {
favoriteNote: Option<String>,
}
#[derive(Default, IntoJava)]
#[derive(Serialize)]
#[allow(non_snake_case)]
#[jnix(package = "com.hippo.ehviewer.client.parser")]
pub struct GalleryListResult {
prev: Option<String>,
next: Option<String>,
@@ -193,57 +190,50 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option<BaseGalleryInfo> {
})
}
pub fn parse_info_list(dom: &VDom, parser: &Parser, str: &str) -> Option<GalleryListResult> {
check_html(str);
if str.contains("<p>You do not have any watched tags") {
panic!("No watched tags!")
}
if str.contains("No hits found</p>") || str.contains("No unfiltered results found") {
panic!("No hits found!")
}
let itg = get_vdom_first_element_by_class_name(dom, "itg")?;
let children = itg.children()?;
let iter = children.top().iter();
let info: Vec<BaseGalleryInfo> = iter
.filter_map(|x| parse_gallery_info(x.get(parser)?, parser))
.collect();
let prev = dom.get_element_by_id("uprev").and_then(|e| {
let str = get_node_handle_attr(&e, parser, "href")?;
Some(
regex!("prev=(\\d+(-\\d+)?)")
.captures(str)?
.index(1)
.to_string(),
)
});
let next = dom.get_element_by_id("unext").and_then(|e| {
let str = get_node_handle_attr(&e, parser, "href")?;
Some(
regex!("next=(\\d+(-\\d+)?)")
.captures(str)?
.index(1)
.to_string(),
)
});
(!info.is_empty()).then_some(GalleryListResult {
prev,
next,
galleryInfoList: info,
})
}
#[no_mangle]
#[catch_panic(default = "std::ptr::null_mut()")]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.GalleryListParserKt")]
pub fn parseGalleryInfoList(
env: JNIEnv,
_class: JClass,
buffer: JByteBuffer,
limit: jint,
) -> jobject {
let mut env = JnixEnv { env };
parse_bytebuffer(&mut env, buffer, limit, |dom, parser, _, str| {
check_html(str);
if str.contains("<p>You do not have any watched tags") {
panic!("No watched tags!")
}
if str.contains("No hits found</p>") || str.contains("No unfiltered results found") {
panic!("No hits found!")
}
let itg = get_vdom_first_element_by_class_name(dom, "itg")?;
let children = itg.children()?;
let iter = children.top().iter();
let info: Vec<BaseGalleryInfo> = iter
.filter_map(|x| parse_gallery_info(x.get(parser)?, parser))
.collect();
let prev = dom.get_element_by_id("uprev").and_then(|e| {
let str = get_node_handle_attr(&e, parser, "href")?;
Some(
regex!("prev=(\\d+(-\\d+)?)")
.captures(str)?
.index(1)
.to_string(),
)
});
let next = dom.get_element_by_id("unext").and_then(|e| {
let str = get_node_handle_attr(&e, parser, "href")?;
Some(
regex!("next=(\\d+(-\\d+)?)")
.captures(str)?
.index(1)
.to_string(),
)
});
(!info.is_empty()).then_some(GalleryListResult {
prev,
next,
galleryInfoList: info,
})
pub fn parseGalleryInfoList(mut env: JNIEnv, _: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, parser, str| {
parse_info_list(dom, parser, str)
})
.unwrap()
.into_java(&env)
.forget()
.into_raw()
}
+34 -34
View File
@@ -1,17 +1,14 @@
use catch_panic::catch_panic;
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use jnix::jni::objects::{JByteBuffer, JClass};
use jnix::jni::sys::{jint, jobject};
use jnix::jni::JNIEnv;
use jnix::{IntoJava, JnixEnv};
use jnix_macros::IntoJava;
use parse_bytebuffer;
use quick_xml::escape::unescape;
use regex;
use serde::Serialize;
use tl::{Parser, VDom};
use {parse_marshal_inplace, regex};
#[derive(Default, IntoJava)]
#[jnix(package = "com.hippo.ehviewer.client.parser")]
pub struct Torrent {
#[derive(Serialize)]
struct Torrent {
posted: String,
size: String,
seeds: i32,
@@ -21,30 +18,33 @@ pub struct Torrent {
name: String,
}
fn parse_torrent_list(dom: &VDom, parser: &Parser) -> Option<Vec<Torrent>> {
let list = dom.query_selector("table")?.filter_map(|e| {
let html = e.get(parser)?.inner_html(parser);
if html.contains("Expunged") {
None
} else {
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]iB)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html).unwrap();
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
}
}).collect::<Vec<Torrent>>();
Some(list)
}
#[no_mangle]
#[catch_panic(default = "std::ptr::null_mut()")]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.TorrentParserKt")]
pub fn parseTorrent(env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jobject {
let mut env = JnixEnv { env };
parse_bytebuffer(&mut env, buffer, limit, |dom, parser, _, _| {
Some(dom.query_selector("table")?.filter_map(|e| {
let html = e.get(parser)?.inner_html(parser);
if html.contains("Expunged") {
None
} else {
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]iB)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html).unwrap();
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
}
}).collect::<Vec<Torrent>>())
}).unwrap().into_java(&env).forget().into_raw()
pub fn parseTorrent(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, parser, _html| {
parse_torrent_list(dom, parser)
})
}