Fix the crash caused by Kotlinx Serialization on KitKat devices. (#1698)

Fix the crash in MangaPlus on KitKat devices
This commit is contained in:
Alessandro Jean 2019-10-23 18:22:54 -03:00 committed by arkon
parent 89a0b734f5
commit 23a9d3d7c4
3 changed files with 207 additions and 5 deletions

View File

@ -6,12 +6,14 @@ ext {
appName = 'Tachiyomi: MANGA Plus by SHUEISHA' appName = 'Tachiyomi: MANGA Plus by SHUEISHA'
pkgNameSuffix = 'all.mangaplus' pkgNameSuffix = 'all.mangaplus'
extClass = '.MangaPlusFactory' extClass = '.MangaPlusFactory'
extVersionCode = 2 extVersionCode = 3
libVersion = '1.2' libVersion = '1.2'
} }
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0'
compileOnly 'com.google.code.gson:gson:2.8.2'
compileOnly project(':duktape-stub')
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.extension.all.mangaplus package eu.kanade.tachiyomi.extension.all.mangaplus
import android.os.Build
import com.google.gson.Gson
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
@ -30,6 +33,12 @@ abstract class MangaPlus(override val lang: String,
.addInterceptor { imageIntercept(it) } .addInterceptor { imageIntercept(it) }
.build() .build()
private val protobufJs: String by lazy {
client.newCall(GET(PROTOBUFJS_CDN, headers)).execute().body()!!.string()
}
private val gson: Gson by lazy { Gson() }
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/title_list/ranking", headers) return GET("$baseUrl/title_list/ranking", headers)
} }
@ -240,8 +249,8 @@ abstract class MangaPlus(override val lang: String,
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray { private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray {
val keyStream = HEX_GROUP val keyStream = HEX_GROUP
.findAll(encryptionKey) .findAll(encryptionKey)
.map { it.groupValues[1].toInt(16) }
.toList() .toList()
.map { it.groupValues[1].toInt(16) }
val content = image val content = image
.map { it.toInt() } .map { it.toInt() }
@ -256,7 +265,33 @@ abstract class MangaPlus(override val lang: String,
return ByteArray(content.size) { pos -> content[pos].toByte() } return ByteArray(content.size) { pos -> content[pos].toByte() }
} }
private fun Response.asProto(): MangaPlusResponse = ProtoBuf.load(MangaPlusSerializer, body()!!.bytes()) private fun Response.asProto(): MangaPlusResponse {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT)
return ProtoBuf.load(MangaPlusSerializer, body()!!.bytes())
// Apparently, the version used of Kotlinx Serialization lib causes a crash
// on KitKat devices (see #1678). So, if the device is running KitKat or lower,
// we use the old method of parsing their API -- using ProtobufJS + Duktape + Gson.
val bytes = body()!!.bytes()
val messageBytes = "var BYTE_ARR = new Uint8Array([${bytes.joinToString()}]);"
val res = Duktape.create().use {
it.set("helper", DuktapeHelper::class.java, object : DuktapeHelper {
override fun getProtobuf(): String = protobufJs
})
it.evaluate(messageBytes + DECODE_SCRIPT) as String
}
// The Json.parse method of the Kotlinx Serialization causes the app to crash too,
// so unfortunately we have to use Gson to deserialize.
return gson.fromJson(res, MangaPlusResponse::class.java)
}
private interface DuktapeHelper {
@Suppress("unused")
fun getProtobuf(): String
}
companion object { companion object {
private const val WEB_URL = "https://mangaplus.shueisha.co.jp" private const val WEB_URL = "https://mangaplus.shueisha.co.jp"
@ -264,5 +299,7 @@ abstract class MangaPlus(override val lang: String,
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36" private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36"
private val HEX_GROUP = "(.{1,2})".toRegex() private val HEX_GROUP = "(.{1,2})".toRegex()
private const val PROTOBUFJS_CDN = "https://cdn.rawgit.com/dcodeIO/protobuf.js/6.8.8/dist/light/protobuf.min.js"
} }
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.mangaplus package eu.kanade.tachiyomi.extension.all.mangaplus
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Optional import kotlinx.serialization.Optional
import kotlinx.serialization.SerialId import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -80,8 +81,13 @@ data class Title(
) )
enum class Language(val id: Int) { enum class Language(val id: Int) {
@SerialId(0) ENGLISH(0), @SerialId(0)
@SerialId(1) SPANISH(1) @SerializedName("0")
ENGLISH(0),
@SerialId(1)
@SerializedName("1")
SPANISH(1)
} }
@Serializable @Serializable
@ -115,3 +121,160 @@ data class MangaPage(
@SerialId(3) val height: Int, @SerialId(3) val height: Int,
@Optional @SerialId(5) val encryptionKey: String? = null @Optional @SerialId(5) val encryptionKey: String? = null
) )
// Used for the deserialization on KitKat devices.
const val DECODE_SCRIPT: String = """
Duktape.modSearch = function(id) {
if (id == "protobufjs")
return helper.getProtobuf();
throw new Error("Cannot find module: " + id);
}
var protobuf = require("protobufjs");
var Root = protobuf.Root;
var Type = protobuf.Type;
var Field = protobuf.Field;
var Enum = protobuf.Enum;
var OneOf = protobuf.OneOf;
var Response = new Type("Response")
.add(
new OneOf("data")
.add(new Field("success", 1, "SuccessResult"))
.add(new Field("error", 2, "ErrorResult"))
);
var ErrorResult = new Type("ErrorResult")
.add(new Field("action", 1, "Action"))
.add(new Field("englishPopup", 2, "Popup"))
.add(new Field("spanishPopup", 3, "Popup"));
var Action = new Enum("Action")
.add("DEFAULT", 0)
.add("UNAUTHORIZED", 1)
.add("MAINTAINENCE", 2)
.add("GEOIP_BLOCKING", 3);
var Popup = new Type("Popup")
.add(new Field("subject", 1, "string"))
.add(new Field("body", 2, "string"));
var SuccessResult = new Type("SuccessResult")
.add(new Field("isFeaturedUpdated", 1, "bool"))
.add(
new OneOf("data")
.add(new Field("allTitlesView", 5, "AllTitlesView"))
.add(new Field("titleRankingView", 6, "TitleRankingView"))
.add(new Field("titleDetailView", 8, "TitleDetailView"))
.add(new Field("mangaViewer", 10, "MangaViewer"))
.add(new Field("webHomeView", 11, "WebHomeView"))
);
var TitleRankingView = new Type("TitleRankingView")
.add(new Field("titles", 1, "Title", "repeated"));
var AllTitlesView = new Type("AllTitlesView")
.add(new Field("titles", 1, "Title", "repeated"));
var WebHomeView = new Type("WebHomeView")
.add(new Field("groups", 2, "UpdatedTitleGroup", "repeated"));
var TitleDetailView = new Type("TitleDetailView")
.add(new Field("title", 1, "Title"))
.add(new Field("titleImageUrl", 2, "string"))
.add(new Field("overview", 3, "string"))
.add(new Field("backgroundImageUrl", 4, "string"))
.add(new Field("nextTimeStamp", 5, "uint32"))
.add(new Field("updateTiming", 6, "UpdateTiming"))
.add(new Field("viewingPeriodDescription", 7, "string"))
.add(new Field("firstChapterList", 9, "Chapter", "repeated"))
.add(new Field("lastChapterList", 10, "Chapter", "repeated"))
.add(new Field("isSimulReleased", 14, "bool"))
.add(new Field("chaptersDescending", 17, "bool"));
var UpdateTiming = new Enum("UpdateTiming")
.add("NOT_REGULARLY", 0)
.add("MONDAY", 1)
.add("TUESDAY", 2)
.add("WEDNESDAY", 3)
.add("THURSDAY", 4)
.add("FRIDAY", 5)
.add("SATURDAY", 6)
.add("SUNDAY", 7)
.add("DAY", 8);
var MangaViewer = new Type("MangaViewer")
.add(new Field("pages", 1, "Page", "repeated"));
var Title = new Type("Title")
.add(new Field("titleId", 1, "uint32"))
.add(new Field("name", 2, "string"))
.add(new Field("author", 3, "string"))
.add(new Field("portraitImageUrl", 4, "string"))
.add(new Field("landscapeImageUrl", 5, "string"))
.add(new Field("viewCount", 6, "uint32"))
.add(new Field("language", 7, "Language", {"default": 0}));
var Language = new Enum("Language")
.add("ENGLISH", 0)
.add("SPANISH", 1);
var UpdatedTitleGroup = new Type("UpdatedTitleGroup")
.add(new Field("groupName", 1, "string"))
.add(new Field("titles", 2, "UpdatedTitle", "repeated"));
var UpdatedTitle = new Type("UpdatedTitle")
.add(new Field("title", 1, "Title"))
.add(new Field("chapterId", 2, "uint32"))
.add(new Field("chapterName", 3, "string"))
.add(new Field("chapterSubtitle", 4, "string"));
var Chapter = new Type("Chapter")
.add(new Field("titleId", 1, "uint32"))
.add(new Field("chapterId", 2, "uint32"))
.add(new Field("name", 3, "string"))
.add(new Field("subTitle", 4, "string", "optional"))
.add(new Field("startTimeStamp", 6, "uint32"))
.add(new Field("endTimeStamp", 7, "uint32"));
var Page = new Type("Page")
.add(new Field("page", 1, "MangaPage"));
var MangaPage = new Type("MangaPage")
.add(new Field("imageUrl", 1, "string"))
.add(new Field("width", 2, "uint32"))
.add(new Field("height", 3, "uint32"))
.add(new Field("encryptionKey", 5, "string", "optional"));
var root = new Root()
.define("mangaplus")
.add(Response)
.add(ErrorResult)
.add(Action)
.add(Popup)
.add(SuccessResult)
.add(TitleRankingView)
.add(AllTitlesView)
.add(WebHomeView)
.add(TitleDetailView)
.add(UpdateTiming)
.add(MangaViewer)
.add(Title)
.add(Language)
.add(UpdatedTitleGroup)
.add(UpdatedTitle)
.add(Chapter)
.add(Page)
.add(MangaPage);
function decode(arr) {
var Response = root.lookupType("Response");
var message = Response.decode(arr);
return Response.toObject(message, {defaults: true});
}
(function () {
return JSON.stringify(decode(BYTE_ARR)).replace(/\,\{\}/g, "");
})();
"""