Compare commits

..

11 Commits

Author SHA1 Message Date
Aria Moradi bc2072e81f bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-09-19 17:36:46 +04:30
Aria Moradi f36bc3f643 update WebUI 2021-09-19 17:34:18 +04:30
Aria Moradi f7901ad843 fix windows paths 2021-09-19 16:43:16 +04:30
Aria Moradi 3771030ed6 closes #202 2021-09-19 14:24:13 +04:30
Aria Moradi 57197e58b5 fix Task path 2021-09-19 14:14:42 +04:30
Aria Moradi ac601399ac update WebUI 2021-09-19 14:14:21 +04:30
Aria Moradi 6a0e221153 fix compile 2021-09-19 01:01:20 +04:30
Aria Moradi 6a949fc851 Minor cleanup 2021-09-19 00:59:04 +04:30
Aria Moradi f1a077dc2f update CHANGELOG 2021-09-18 22:09:34 +04:30
Mitchell Syer f20962b02b Gradle Updates (#199)
* Cleanup and update gradle, update dependencies

* Duplicate Jsoup
2021-09-18 22:07:19 +04:30
Mitchell Syer 77e057f244 Update BytecodeEditor to use Java NIO Paths (#200) 2021-09-18 21:57:15 +04:30
11 changed files with 168 additions and 250 deletions
+4 -44
View File
@@ -1,68 +1,28 @@
plugins {
application
kotlin("plugin.serialization")
}
repositories {
mavenCentral()
maven {
url = uri("https://jitpack.io")
}
maven {
url = uri("https://maven.google.com")
}
}
dependencies { dependencies {
// Android stub library // Android stub library
implementation(fileTree("lib/")) implementation(fileTree("lib/"))
// JSON
compileOnly("com.google.code.gson:gson:2.8.6")
// XML // XML
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1") compileOnly("xmlpull:xmlpull:1.1.3.4a")
// Config API // Config API
implementation(project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// APK sig verifier // APK sig verifier
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13") compileOnly("com.android.tools.build:apksig:7.1.0-alpha12")
// AndroidX annotations // AndroidX annotations
compileOnly("androidx.annotation:annotation:1.2.0-alpha01") compileOnly("androidx.annotation:annotation:1.2.0")
// substitute for duktape-android // substitute for duktape-android
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino' implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.7.7" val multiplatformSettingsVersion = "0.8"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
// Android version of SimpleDateFormat // Android version of SimpleDateFormat
implementation("com.ibm.icu:icu4j:69.1") implementation("com.ibm.icu:icu4j:69.1")
} }
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
//
//// Copy JVM core patches
//task copyJVMPatches(type: Copy) {
// from fatJarTask.outputs.files
// into 'src/main/resources/patches'
//}
//
//compileOnly(Java.dependsOn gradle.includedBuild('dex2jar').task(':dex-translator:assemble')
//compileOnly(Java.dependsOn copyJVMPatches
//copyJVMPatches.dependsOn fatJarTask
//
@@ -13,7 +13,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public class TwoStatePreference extends Preference { public class TwoStatePreference extends Preference {
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them // Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
public TwoStatePreference(Context context) { super(context); } public TwoStatePreference(Context context) {
super(context);
setDefaultValue(false);
}
@JsonIgnore @JsonIgnore
public boolean isChecked() { throw new RuntimeException("Stub!"); } public boolean isChecked() { throw new RuntimeException("Stub!"); }
+39 -5
View File
@@ -1,10 +1,44 @@
# Server: v0.5.2 + WebUI: r807
## TL;DR
- Fixed Local source not working on Windows
- Fixed Chapter numbers being shown incorrectly
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
- N/A
#### Bug fixes
- N/A
### Private API
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @Syer10)
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @Syer10)
## Tachidesk-WebUI
#### Visible changes
- (r804) update text positioning on Reader and Player ([#35](https://github.com/Suwayomi/Tachidesk-WebUI/pull/35) by @voltrare)
- (r806) Source card for Local source is different
- (r807) add Local source guide
#### Bug fixes
- (r805) fix chapter name
#### Internal changes
- N/A
# Server: v0.5.1 + WebUI: r803 # Server: v0.5.1 + WebUI: r803
## TL;DR ## TL;DR
- Loading sources' manga list is at least twice as fast - Loading sources' manga list is at least twice as fast
- Added support for Tachiyomi's Local source - Added support for Tachiyomi's Local source
- Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server - Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server
- Added ability to turn off cache for image requests - Added ability to turn off cache for image requests
<!-- TODO: fill before release -->
## Tachidesk-Server ## Tachidesk-Server
### Public API ### Public API
@@ -32,14 +66,14 @@
#### Visible changes #### Visible changes
- (r790) nice looking progress percentage - (r790) nice looking progress percentage
- (r791) show a Delete button for downloaded chapters - (r791) show a Delete button for downloaded chapters
- (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @voltrare) - (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/29) by @voltrare)
- (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @phanirithvij) - (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/32) by @phanirithvij)
- (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @phanirithvij) - (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31) by @phanirithvij)
- (r795) fix viewing page number when the string is long - (r795) fix viewing page number when the string is long
- (r796) show proper display name for source - (r796) show proper display name for source
- (r797) fail gracefully when a thumbnail has errors - (r797) fail gracefully when a thumbnail has errors
- (r798) fix when a source fails to load mangas - (r798) fix when a source fails to load mangas
- (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21)) - (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31))
- (r803) add support for useCache - (r803) add support for useCache
#### Bug fixes #### Bug fixes
+31 -14
View File
@@ -1,8 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins { plugins {
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0"
} }
allprojects { allprojects {
@@ -12,10 +15,8 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://maven.google.com/") google()
maven("https://jitpack.io") maven("https://jitpack.io")
maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://dl.google.com/dl/android/maven2/")
} }
} }
@@ -27,18 +28,36 @@ val projects = listOf(
configure(projects) { configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
tasks.withType<KotlinCompile> { tasks {
kotlinOptions { withType<KotlinCompile> {
jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
} }
} }
dependencies { dependencies {
// Kotlin // Kotlin
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
@@ -46,7 +65,7 @@ configure(projects) {
testImplementation(kotlin("test-junit5")) testImplementation(kotlin("test-junit5"))
// coroutines // coroutines
val coroutinesVersion = "1.5.1" val coroutinesVersion = "1.5.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
@@ -55,14 +74,13 @@ configure(projects) {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.7.0") implementation("org.kodein.di:kodein-di-conf-jvm:7.8.0")
// Logging // Logging
implementation("org.slf4j:slf4j-api:1.7.30") implementation("org.slf4j:slf4j-api:1.7.32")
implementation("ch.qos.logback:logback-classic:1.2.3") implementation("ch.qos.logback:logback-classic:1.2.6")
implementation("io.github.microutils:kotlin-logging:2.0.6") implementation("io.github.microutils:kotlin-logging:2.0.11")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
@@ -70,7 +88,7 @@ configure(projects) {
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions // dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
implementation("org.jsoup:jsoup:1.14.1") implementation("org.jsoup:jsoup:1.14.2")
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1") implementation("com.typesafe:config:1.4.1")
@@ -87,7 +105,6 @@ configure(projects) {
// APK parser // APK parser
implementation("net.dongliu:apk-parser:2.6.10") implementation("net.dongliu:apk-parser:2.6.10")
// dependency both in AndroidCompat and server, version locked by javalin // dependency both in AndroidCompat and server, version locked by javalin
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4") implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
} }
+2 -2
View File
@@ -12,9 +12,9 @@ const val kotlinVersion = "1.5.30"
const val MainClass = "suwayomi.tachidesk.MainKt" const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.1" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.2"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r803" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r807"
// counts commits on the master branch // counts commits on the master branch
val tachideskRevision = runCatching { val tachideskRevision = runCatching {
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+17 -50
View File
@@ -1,25 +1,11 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import de.undercouch.gradle.tasks.download.Download
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.time.Instant import java.time.Instant
plugins { plugins {
application application
kotlin("plugin.serialization")
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.6.0" id("com.github.gmazzo.buildconfig") version "3.0.3"
id("com.github.gmazzo.buildconfig") version "3.0.2"
}
repositories {
maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven {
url = uri("https://jitpack.io")
}
} }
dependencies { dependencies {
@@ -33,8 +19,9 @@ dependencies {
// Javalin api // Javalin api
implementation("io.javalin:javalin:4.0.0") implementation("io.javalin:javalin:4.0.0")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` // jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.4") val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.4") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
// Exposed ORM // Exposed ORM
val exposedVersion = "0.34.1" val exposedVersion = "0.34.1"
@@ -46,8 +33,7 @@ dependencies {
implementation("com.h2database:h2:1.4.200") implementation("com.h2database:h2:1.4.200")
// Exposed Migrations // Exposed Migrations
val exposedMigrationsVersion = "3.1.2" implementation("com.github.Suwayomi:exposed-migrations:3.1.2")
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
// tray icon // tray icon
implementation("com.dorkbox:SystemTray:4.1") implementation("com.dorkbox:SystemTray:4.1")
@@ -57,8 +43,8 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1") implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.14.1") implementation("org.jsoup:jsoup:1.14.2")
implementation("com.google.code.gson:gson:2.8.7") implementation("com.google.code.gson:gson:2.8.8")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0") implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Sort // Sort
@@ -131,29 +117,17 @@ tasks {
shadowJar { shadowJar {
manifest { manifest {
attributes( attributes(
mapOf( "Main-Class" to MainClass,
"Main-Class" to MainClass, "Implementation-Title" to rootProject.name,
"Implementation-Title" to rootProject.name, "Implementation-Vendor" to "The Suwayomi Project",
"Implementation-Vendor" to "The Suwayomi Project", "Specification-Version" to tachideskVersion,
"Specification-Version" to tachideskVersion, "Implementation-Version" to tachideskRevision
"Implementation-Version" to tachideskRevision
)
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion) archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision) archiveClassifier.set(tachideskRevision)
} }
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
test { test {
useJUnit() useJUnit()
@@ -164,7 +138,7 @@ tasks {
} }
named("run") { named("run") {
dependsOn("formatKotlin", "lintKotlin") dependsOn(":formatKotlin", ":lintKotlin")
} }
named<Copy>("processResources") { named<Copy>("processResources") {
@@ -172,7 +146,7 @@ tasks {
mustRunAfter("downloadWebUI") mustRunAfter("downloadWebUI")
} }
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") { register<Download>("downloadWebUI") {
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip") src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
dest("src/main/resources/WebUI.zip") dest("src/main/resources/WebUI.zip")
@@ -187,8 +161,9 @@ tasks {
it.readText().trim() it.readText().trim()
} }
if (zipRevision == webUIRevisionTag) if (zipRevision == webUIRevisionTag) {
shouldOverwrite = false shouldOverwrite = false
}
} }
return shouldOverwrite return shouldOverwrite
@@ -196,12 +171,4 @@ tasks {
overwrite(shouldOverwrite()) overwrite(shouldOverwrite())
} }
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
}
} }
@@ -32,7 +32,10 @@ import okhttp3.OkHttpClient
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import okio.buffer
import okio.source
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -50,7 +53,7 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.net.URL import java.net.URLDecoder
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile import java.util.zip.ZipFile
@@ -342,18 +345,14 @@ class LocalSource : HttpSource() {
throw Exception("Chapter not found") throw Exception("Chapter not found")
} }
private fun getFormat(file: File): Format { private fun getFormat(file: File): Format = with(file) {
return with(file) { when {
when { isDirectory -> Format.Directory(this)
isDirectory -> Format.Directory(file) extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
extension.equals("zip", true) -> Format.Zip(file) extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
extension.equals("cbz", true) -> Format.Zip(file) extension.equals("epub", true) -> Format.Epub(this)
extension.equals("rar", true) -> Format.Rar(file)
extension.equals("cbr", true) -> Format.Rar(file)
extension.equals("epub", true) -> Format.Epub(file)
else -> throw Exception("Invalid chapter format") else -> throw Exception("Invalid chapter format")
}
} }
} }
@@ -439,19 +438,28 @@ class LocalSource : HttpSource() {
} }
private object FileSystemInterceptor : Interceptor { private object FileSystemInterceptor : Interceptor {
fun fakeUrlFrom(path: String) = "http://$path" fun fakeUrlFrom(path: String): String = "http://$path"
private fun restoreFileUrl(markedFakeHttpUrl: String): String {
return markedFakeHttpUrl.replaceFirst("http:", "file:/") private fun restoreFilePath(url: String): String {
val path = URLDecoder.decode(url.replaceFirst("http://", ""), "UTF-8")
// Windows
if (System.getProperty("os.name").lowercase().startsWith("win")) {
// convert paths like "c/Users/..." to "c:/Users/..."
return StringBuilder(path).insert(1, ":").toString()
}
return "/$path"
} }
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
val url = request.url val url = request.url
val fileUrl = restoreFileUrl(url.toString()) val filePath = restoreFilePath(url.toString())
return try { return try {
Response.Builder() Response.Builder()
.body(URL(fileUrl).readBytes().toResponseBody()) .body(File(filePath).source().buffer().asResponseBody())
.code(200) .code(200)
.message("Some file") .message("Some file")
.protocol(Protocol.HTTP_1_0) .protocol(Protocol.HTTP_1_0)
@@ -461,7 +469,7 @@ private object FileSystemInterceptor : Interceptor {
Response.Builder() Response.Builder()
.body("".toResponseBody()) .body("".toResponseBody())
.code(404) .code(404)
.message(e.message ?: "File not found ($fileUrl)") .message(e.message ?: "File not found ($filePath)")
.protocol(Protocol.HTTP_1_0) .protocol(Protocol.HTTP_1_0)
.request(request) .request(request)
.build() .build()
@@ -82,7 +82,7 @@ object PackageTools {
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
} else { } else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) BytecodeEditor.fixAndroidClasses(jarFilePath)
} }
} }
@@ -15,15 +15,10 @@ import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.Handle import org.objectweb.asm.Handle
import org.objectweb.asm.MethodVisitor import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode import java.nio.file.FileSystems
import suwayomi.tachidesk.manga.impl.util.storage.use import java.nio.file.Files
import java.io.File import java.nio.file.Path
import java.io.IOException import kotlin.streams.asSequence
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
object BytecodeEditor { object BytecodeEditor {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -33,77 +28,52 @@ object BytecodeEditor {
* *
* @param jarFile The JarFile to replace class references in * @param jarFile The JarFile to replace class references in
*/ */
fun fixAndroidClasses(jarFile: File) { fun fixAndroidClasses(jarFile: Path) {
val nodes = loadClasses(jarFile) FileSystems.newFileSystem(jarFile, null as ClassLoader?)?.use {
.mapValues { (className, classFileBuffer) -> Files.walk(it.getPath("/")).asSequence()
logger.trace { "Processing class $className" } .filterNotNull()
transform(classFileBuffer) .filterNot(Files::isDirectory)
} + loadNonClasses(jarFile) .mapNotNull(::getClassBytes)
.map(::transform)
saveAsJar(nodes, jarFile) .forEach(::write)
}
/**
* Load all classes inside the [jar] [File]
*
* @param jar The JarFile to load classes from
*
* @return [Map] with class names and [ByteArray]s of bytecode
*/
private fun loadClasses(jar: File): Map<String, ByteArray> {
return JarFile(jar).use { jarFile ->
jarFile.entries()
.asSequence()
.mapNotNull {
readJar(jarFile, it)
}
.toMap()
} }
} }
/** /**
* Get class file in [jar] for [entry] * Get class bytes from a [Path]
* *
* @param jar The jar to get the class from * @param path The path entry to get the class bytes from
* @param entry The entry in the jar
* *
* @return [Pair] of the class name plus the class [ByteArray], or null if it's not a valid class * @return [Pair] of the [Path] plus the class [ByteArray], or null if it's not a valid class
*/ */
private fun readJar(jar: JarFile, entry: JarEntry): Pair<String, ByteArray>? { private fun getClassBytes(path: Path): Pair<Path, ByteArray>? {
return try { return try {
jar.getInputStream(entry).use { stream -> if (path.toString().endsWith(".class")) {
if (entry.name.endsWith(".class")) { val bytes = Files.readAllBytes(path)
val bytes = stream.readBytes() if (bytes.size < 4) {
if (bytes.size < 4) { // Invalid class size
// Invalid class size return null
return@use null }
} val cafebabe = String.format(
val cafebabe = String.format( "%02X%02X%02X%02X",
"%02X%02X%02X%02X", bytes[0],
bytes[0], bytes[1],
bytes[1], bytes[2],
bytes[2], bytes[3]
bytes[3] )
) if (cafebabe.lowercase() != "cafebabe") {
if (cafebabe.lowercase() != "cafebabe") { // Corrupted class
// Corrupted class return null
return@use null }
}
getNode(bytes).name to bytes path to bytes
} else null } else null
} } catch (e: Exception) {
} catch (e: IOException) { logger.error(e) { "Error loading class from Path: $path" }
logger.error(e) { "Error loading jar file" }
null null
} }
} }
private fun getNode(bytes: ByteArray): ClassNode {
val cr = ClassReader(bytes)
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
}
/** /**
* The path where replacement classes will reside * The path where replacement classes will reside
*/ */
@@ -153,9 +123,9 @@ object BytecodeEditor {
* *
* @return [ByteArray] with modified bytecode * @return [ByteArray] with modified bytecode
*/ */
private fun transform(classfileBuffer: ByteArray): ByteArray { private fun transform(pair: Pair<Path, ByteArray>): Pair<Path, ByteArray> {
// Read the class and prepare to modify it // Read the class and prepare to modify it
val cr = ClassReader(classfileBuffer) val cr = ClassReader(pair.second)
val cw = ClassWriter(cr, 0) val cw = ClassWriter(cr, 0)
// Modify the class // Modify the class
cr.accept( cr.accept(
@@ -277,51 +247,10 @@ object BytecodeEditor {
}, },
0 0
) )
return cw.toByteArray() return pair.first to cw.toByteArray()
} }
/** private fun write(pair: Pair<Path, ByteArray>) {
* Load non-class files from the jar, such as icons and the manifest Files.write(pair.first, pair.second)
*
* @param [jarFile] The file to load resources from
*
* @return [Map] of resources
*/
private fun loadNonClasses(jarFile: File): Map<String, ByteArray> {
val entries = mutableMapOf<String, ByteArray>()
ZipInputStream(jarFile.inputStream()).use { stream ->
var nextEntry: ZipEntry?
while (stream.nextEntry.also { nextEntry = it } != null) {
nextEntry?.use(stream) { entry ->
// If it ends with class or is a directory ignore it
if (!entry.name.endsWith(".class") && !entry.isDirectory) {
val bytes = stream.readBytes()
entries[entry.name] = bytes
}
}
}
}
return entries
}
/**
* Save jar with modified content
*
* @param outBytes [Map] of names and [ByteArray]s of content to save inside the jar
* @param file JarFile to save to
*/
private fun saveAsJar(outBytes: Map<String, ByteArray>, file: File) {
JarOutputStream(file.outputStream()).use { out ->
outBytes.forEach { (entry, value) ->
// Append extension to class entries
out.putNextEntry(
ZipEntry(
entry + if (entry.contains(".")) "" else ".class"
)
)
out.write(value)
out.closeEntry()
}
}
} }
} }
@@ -82,7 +82,7 @@ object PackageTools {
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
} else { } else {
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) BytecodeEditor.fixAndroidClasses(jarFilePath)
} }
} }