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 42d5e630b..b28cd2b7f 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.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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt index 2b8f21315..63ba2c4e8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ByteBufferSource.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ByteBufferSource.kt new file mode 100644 index 000000000..ac81befa3 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ByteBufferSource.kt @@ -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 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() } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/FavoritesParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/FavoritesParser.kt index a652fc3f5..3da9a1b11 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/FavoritesParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/FavoritesParser.kt @@ -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, + val countArray: List, + 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(10) - val countArray = runSuspendCatching { - parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) } + fun parse(body: ByteBuffer): FavParserResult { + return runSuspendCatching { + unmarshalParsingAs(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, - 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): IntArray +private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryListParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryListParser.kt index 6ff0167a9..1ad94811c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryListParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryListParser.kt @@ -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(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, ) -private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): GalleryListResult +private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt index fa70620d9..e70220cc6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt @@ -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(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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/TorrentParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/TorrentParser.kt index d91314185..9e47ee8d3 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/TorrentParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/TorrentParser.kt @@ -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(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 -private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList +private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): Int diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt index a37fbef48..c36e3f8cd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt @@ -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) diff --git a/app/src/main/rust/Cargo.toml b/app/src/main/rust/Cargo.toml index 20048631b..9c3e05898 100644 --- a/app/src/main/rust/Cargo.toml +++ b/app/src/main/rust/Cargo.toml @@ -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 diff --git a/app/src/main/rust/catch_panic/Cargo.toml b/app/src/main/rust/catch_panic/Cargo.toml deleted file mode 100644 index f0075b086..000000000 --- a/app/src/main/rust/catch_panic/Cargo.toml +++ /dev/null @@ -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 ", - "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" } diff --git a/app/src/main/rust/catch_panic/src/handler.rs b/app/src/main/rust/catch_panic/src/handler.rs deleted file mode 100644 index 2fbf603e6..000000000 --- a/app/src/main/rust/catch_panic/src/handler.rs +++ /dev/null @@ -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(env: JNIEnv, default: R, handler: H, f: F) -> R -where - F: FnOnce() -> R + UnwindSafe, - H: FnOnce(JNIEnv, Box), -{ - 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) { - let msg = match err.downcast_ref::<&'static str>() { - Some(s) => *s, - None => match err.downcast_ref::() { - Some(s) => &s[..], - None => "Box", - }, - }; - env.throw_new("java/lang/RuntimeException", msg).unwrap(); -} diff --git a/app/src/main/rust/catch_panic/src/lib.rs b/app/src/main/rust/catch_panic/src/lib.rs deleted file mode 100644 index a5936a76d..000000000 --- a/app/src/main/rust/catch_panic/src/lib.rs +++ /dev/null @@ -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) { -//! let msg = match err.downcast_ref::<&'static str>() { -//! Some(s) => *s, -//! None => match err.downcast_ref::() { -//! 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!(""); -//! } -//! -//! # 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; diff --git a/app/src/main/rust/catch_panic_macros/Cargo.toml b/app/src/main/rust/catch_panic_macros/Cargo.toml deleted file mode 100644 index 137878c44..000000000 --- a/app/src/main/rust/catch_panic_macros/Cargo.toml +++ /dev/null @@ -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 ", - "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"] } diff --git a/app/src/main/rust/catch_panic_macros/src/lib.rs b/app/src/main/rust/catch_panic_macros/src/lib.rs deleted file mode 100644 index 174a3d81a..000000000 --- a/app/src/main/rust/catch_panic_macros/src/lib.rs +++ /dev/null @@ -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::()?.parse::()?); - Ok(()) - } else if meta.path.is_ident("handler") { - handler = Some(meta.value()?.parse::()?.parse::()?); - 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::(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 - }) - } - }) -} diff --git a/app/src/main/rust/jnix/Cargo.toml b/app/src/main/rust/jnix/Cargo.toml deleted file mode 100644 index fad21cfec..000000000 --- a/app/src/main/rust/jnix/Cargo.toml +++ /dev/null @@ -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" } diff --git a/app/src/main/rust/jnix/src/as_jvalue.rs b/app/src/main/rust/jnix/src/as_jvalue.rs deleted file mode 100644 index a65fcb119..000000000 --- a/app/src/main/rust/jnix/src/as_jvalue.rs +++ /dev/null @@ -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); diff --git a/app/src/main/rust/jnix/src/from_java/implementations/mod.rs b/app/src/main/rust/jnix/src/from_java/implementations/mod.rs deleted file mode 100644 index 517d00a18..000000000 --- a/app/src/main/rust/jnix/src/from_java/implementations/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod std; diff --git a/app/src/main/rust/jnix/src/from_java/implementations/std/mod.rs b/app/src/main/rust/jnix/src/from_java/implementations/std/mod.rs deleted file mode 100644 index 874951fc2..000000000 --- a/app/src/main/rust/jnix/src/from_java/implementations/std/mod.rs +++ /dev/null @@ -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 -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 -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 { - 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 -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 - } -} diff --git a/app/src/main/rust/jnix/src/from_java/mod.rs b/app/src/main/rust/jnix/src/from_java/mod.rs deleted file mode 100644 index c8758451e..000000000 --- a/app/src/main/rust/jnix/src/from_java/mod.rs +++ /dev/null @@ -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; -} diff --git a/app/src/main/rust/jnix/src/into_java/implementations/mod.rs b/app/src/main/rust/jnix/src/into_java/implementations/mod.rs deleted file mode 100644 index 517d00a18..000000000 --- a/app/src/main/rust/jnix/src/into_java/implementations/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod std; diff --git a/app/src/main/rust/jnix/src/into_java/implementations/std/mod.rs b/app/src/main/rust/jnix/src/into_java/implementations/std/mod.rs deleted file mode 100644 index f59c70215..000000000 --- a/app/src/main/rust/jnix/src/into_java/implementations/std/mod.rs +++ /dev/null @@ -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 -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 -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 -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 -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", ¶meters) - .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) - } -} diff --git a/app/src/main/rust/jnix/src/into_java/mod.rs b/app/src/main/rust/jnix/src/into_java/mod.rs deleted file mode 100644 index ea622b4f0..000000000 --- a/app/src/main/rust/jnix/src/into_java/mod.rs +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/rust/jnix/src/jnix_env.rs b/app/src/main/rust/jnix/src/jnix_env.rs deleted file mode 100644 index 849197969..000000000 --- a/app/src/main/rust/jnix/src/jnix_env.rs +++ /dev/null @@ -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>> = - 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> 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>) -> 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>) { - 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) -> 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", - ) - } -} diff --git a/app/src/main/rust/jnix/src/lib.rs b/app/src/main/rust/jnix/src/lib.rs deleted file mode 100644 index c003fa14f..000000000 --- a/app/src/main/rust/jnix/src/lib.rs +++ /dev/null @@ -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}; diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index 95065a1be..3214e2dd9 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -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 { + 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(env: &mut JnixEnv, str: JByteBuffer, limit: jint, mut f: F) -> Option +fn parse_marshal_inplace(env: &mut JNIEnv, str: JByteBuffer, limit: jint, mut f: F) -> i32 where - F: FnMut(&VDom, &Parser, &JnixEnv, &str) -> Option, + F: FnMut(&VDom, &Parser, &str) -> Option + 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::() { + Some(s) => &s[..], + None => "Box", + }, + }; + env.throw_new("java/lang/RuntimeException", msg).unwrap(); + 0 + } + } } fn get_node_handle_attr<'a>( diff --git a/app/src/main/rust/src/parser/fav.rs b/app/src/main/rust/src/parser/fav.rs index dc8bf11de..29697e431 100644 --- a/app/src/main/rust/src/parser/fav.rs +++ b/app/src/main/rust/src/parser/fav.rs @@ -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, + countArray: Vec, + galleryListResult: GalleryListResult, +} + +fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Option { + if html.contains("This page requires you to log on.

") { + 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::() + .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.

") { - panic!("Not logged in!") - } - let vec: Vec = 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::() - .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()) } diff --git a/app/src/main/rust/src/parser/home.rs b/app/src/main/rust/src/parser/home.rs index 9fc3d5309..d211c85c3 100644 --- a/app/src/main/rust/src/parser/home.rs +++ b/app/src/main/rust/src/parser/home.rs @@ -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 { + let iter = get_vdom_first_element_by_class_name(dom, "homebox")? + .as_tag()? + .query_selector(parser, "strong")?; + let vec: Vec = iter + .filter_map(|e| e.get(parser)?.inner_text(parser).parse::().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 = iter - .filter_map(|e| e.get(parser)?.inner_text(parser).parse::().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() } diff --git a/app/src/main/rust/src/parser/list.rs b/app/src/main/rust/src/parser/list.rs index d1fa6add6..c7f0da9fe 100644 --- a/app/src/main/rust/src/parser/list.rs +++ b/app/src/main/rust/src/parser/list.rs @@ -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, } -#[derive(Default, IntoJava)] +#[derive(Serialize)] #[allow(non_snake_case)] -#[jnix(package = "com.hippo.ehviewer.client.parser")] pub struct GalleryListResult { prev: Option, next: Option, @@ -193,57 +190,50 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option { }) } +pub fn parse_info_list(dom: &VDom, parser: &Parser, str: &str) -> Option { + check_html(str); + if str.contains("

You do not have any watched tags") { + panic!("No watched tags!") + } + if str.contains("No hits found

") || 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 = 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("

You do not have any watched tags") { - panic!("No watched tags!") - } - if str.contains("No hits found

") || 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 = 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() } diff --git a/app/src/main/rust/src/parser/torrent.rs b/app/src/main/rust/src/parser/torrent.rs index ec1a4e699..6f1417684 100644 --- a/app/src/main/rust/src/parser/torrent.rs +++ b/app/src/main/rust/src/parser/torrent.rs @@ -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> { + 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!(" ([0-9-]+) [0-9:]+[\\s\\S]+ ([0-9.]+ [KMGT]iB)[\\s\\S]+ ([0-9]+)[\\s\\S]+ ([0-9]+)[\\s\\S]+ ([0-9]+)[\\s\\S]+([^<]+)[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)").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::>(); + 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!(" ([0-9-]+) [0-9:]+[\\s\\S]+ ([0-9.]+ [KMGT]iB)[\\s\\S]+ ([0-9]+)[\\s\\S]+ ([0-9]+)[\\s\\S]+ ([0-9]+)[\\s\\S]+([^<]+)[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)").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::>()) - }).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) + }) }