You've already forked revanced-cli
mirror of
https://github.com/revanced/revanced-cli
synced 2025-11-19 15:33:27 +01:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63a8aa315d | ||
|
|
491874bb51 | ||
|
|
cb887ead2c | ||
|
|
cd36e710d6 | ||
|
|
1d096c3e92 | ||
|
|
8d29b8cd47 | ||
|
|
0ce680a6f1 | ||
|
|
ddebe4d43c | ||
|
|
bc17298a80 | ||
|
|
536f6f90b1 | ||
|
|
ccf5206da8 | ||
|
|
07714abe49 | ||
|
|
d954424b30 | ||
|
|
168c003a31 | ||
|
|
96ec6a0384 | ||
|
|
1cd0976415 | ||
|
|
d8dbffd7a7 | ||
|
|
5f2bccc4e5 | ||
|
|
5af2f10e14 | ||
|
|
40c8500e38 | ||
|
|
9add2f9e82 | ||
|
|
a942a57364 | ||
|
|
718df54bdc | ||
|
|
507b6f3409 | ||
|
|
c183c38f2a | ||
|
|
d9c5a179c5 | ||
|
|
6447311b0a | ||
|
|
2015c2a1dc | ||
|
|
99f123d1ca | ||
|
|
6ccbec6c95 | ||
|
|
9c30eac7c1 | ||
|
|
a26b0ea64d | ||
|
|
ab4c81965e | ||
|
|
eb83cabfff | ||
|
|
125fa06ca6 | ||
|
|
131100ef00 | ||
|
|
8e91c12c5e | ||
|
|
93a4787780 | ||
|
|
ac7c7a9a1a | ||
|
|
cd3ded1fbd | ||
|
|
bac8c67d6f | ||
|
|
387afd6bd5 | ||
|
|
ef2019ceba | ||
|
|
c1333d87f4 | ||
|
|
00fec2508a | ||
|
|
387b6bd016 | ||
|
|
5e7785fd8f | ||
|
|
19dfa2e2f1 | ||
|
|
0ddc2b54b7 | ||
|
|
3a51ce71a5 | ||
|
|
52b316150d | ||
|
|
75e9c49b72 | ||
|
|
31a05b6768 | ||
|
|
57a1e7c27f | ||
|
|
585d77ce80 | ||
|
|
b4d0ce52ea | ||
|
|
ee26a8d233 | ||
|
|
be3abdda30 | ||
|
|
1849d570f8 | ||
|
|
20fa17957e | ||
|
|
0d58ef14ae | ||
|
|
20e15defc2 | ||
|
|
9f91f63220 | ||
|
|
8782cdef67 | ||
|
|
58fa0774c4 | ||
|
|
dc5ff36058 | ||
|
|
27c28fab5e | ||
|
|
88852a45ac | ||
|
|
8dd9293cb9 | ||
|
|
499ce0a6fb | ||
|
|
27457e0c7d | ||
|
|
7418573c6c | ||
|
|
df85fa37ef | ||
|
|
04805e45fe | ||
|
|
61d3b99313 | ||
|
|
5b8537e6b7 | ||
|
|
7d8a61c3ba | ||
|
|
1dcdbc9fe9 | ||
|
|
4cc2f5269f | ||
|
|
46056956fe | ||
|
|
8e3d147690 | ||
|
|
87ffaa4bdb | ||
|
|
8a49dcc110 | ||
|
|
4a23cb69bc | ||
|
|
8c325af0f9 | ||
|
|
a26ab2a2c3 | ||
|
|
d335846202 | ||
|
|
6fc3ae67c8 | ||
|
|
0df936e99b | ||
|
|
83743929c8 | ||
|
|
51e091ce40 | ||
|
|
e5a37e0a5f | ||
|
|
c94471f464 | ||
|
|
bfd50a43b9 | ||
|
|
2c8a106151 | ||
|
|
ea7efd2afc | ||
|
|
6c4c1924ee | ||
|
|
ce78b245d1 | ||
|
|
d7cffea99c | ||
|
|
9331594706 | ||
|
|
07f6bdf330 | ||
|
|
a48c0860e3 | ||
|
|
4d5efab8bd | ||
|
|
b30c7375a7 | ||
|
|
f694542d64 | ||
|
|
6f54af5963 | ||
|
|
0a52180431 | ||
|
|
af32572f29 | ||
|
|
e126436f3d | ||
|
|
80c11fef73 | ||
|
|
debf0116fb | ||
|
|
29105bab3d | ||
|
|
31853fe539 | ||
|
|
21747d5552 | ||
|
|
ee6aff8fe7 | ||
|
|
f3a3e935a2 | ||
|
|
c272d55e2d | ||
|
|
1781612789 | ||
|
|
3829136c49 | ||
|
|
00145f2bb6 | ||
|
|
7dabd53109 | ||
|
|
98bd6f3f4b | ||
|
|
1a3db77c21 | ||
|
|
430de23856 | ||
|
|
c7d72c4d1c | ||
|
|
09b9027e5e | ||
|
|
3cc98efaa6 |
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a very clearly broken issue.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced bug report
|
||||
|
||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-cli/labels/bug).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Error while running the CLI
|
||||
- Error at runtime
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: How did you find the bug? Any additional details that might help?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Add the steps to reproduce this bug including your environment.
|
||||
placeholder: Step 1. Download some files. Step 2. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📃 Documentation
|
||||
url: https://github.com/revanced/revanced-documentation/
|
||||
about: Don't know how or where to start? Check out our documentation!
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||
about: Got something you think should change or be added? Search for or start a new discussion!
|
||||
46
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
46
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced feature request
|
||||
|
||||
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
|
||||
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-cli/labels/feature-request).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Functionality
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
description: What is the current problem. Why does it require a feature request?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe your feature in detail. How does it solve the issue?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why should your feature should be considered?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -7,7 +7,11 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
["@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{"type": "build", "release": "patch"}
|
||||
]
|
||||
}],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
|
||||
285
CHANGELOG.md
285
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -10,13 +10,7 @@ val githubPassword: String = project.findProperty("gpr.key") as? String ?: Syste
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||
credentials {
|
||||
username = githubUsername
|
||||
password = githubPassword
|
||||
}
|
||||
}
|
||||
mavenLocal()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
@@ -31,7 +25,8 @@ repositories {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("reflect"))
|
||||
implementation("app.revanced:revanced-patcher:1.3.0")
|
||||
|
||||
implementation("app.revanced:revanced-patcher:3.3.1")
|
||||
implementation("info.picocli:picocli:4.6.3")
|
||||
implementation("com.android.tools.build:apksig:7.2.1")
|
||||
implementation("com.github.revanced:jadb:master-SNAPSHOT") // updated fork
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
kotlin.code.style = official
|
||||
version = 1.5.0
|
||||
version = 2.8.3
|
||||
|
||||
12
src/main/kotlin/app/revanced/cli/aligning/Aligning.kt
Normal file
12
src/main/kotlin/app/revanced/cli/aligning/Aligning.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package app.revanced.cli.aligning
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.utils.signing.align.ZipAligner
|
||||
import java.io.File
|
||||
|
||||
object Aligning {
|
||||
fun align(inputFile: File, outputFile: File) {
|
||||
logger.info("Aligning ${inputFile.name} to ${outputFile.name}")
|
||||
ZipAligner.align(inputFile, outputFile)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
8
src/main/kotlin/app/revanced/cli/logging/CliLogger.kt
Normal file
8
src/main/kotlin/app/revanced/cli/logging/CliLogger.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package app.revanced.cli.logging
|
||||
|
||||
internal interface CliLogger {
|
||||
fun error(msg: String)
|
||||
fun info(msg: String)
|
||||
fun trace(msg: String)
|
||||
fun warn(msg: String)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package app.revanced.cli.logging.impl
|
||||
|
||||
import app.revanced.cli.command.MainCommand
|
||||
import app.revanced.cli.logging.CliLogger
|
||||
import java.util.logging.Logger
|
||||
import java.util.logging.SimpleFormatter
|
||||
|
||||
internal class DefaultCliLogger(
|
||||
private val logger: Logger = Logger.getLogger(MainCommand::class.java.name),
|
||||
private val errorLogger: Logger = Logger.getLogger(logger.name + "Err")
|
||||
) : CliLogger {
|
||||
|
||||
init {
|
||||
logger.useParentHandlers = false
|
||||
if (logger.handlers.isEmpty()) {
|
||||
logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter()))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(msg: String) = errorLogger.severe(msg)
|
||||
override fun info(msg: String) = logger.info(msg)
|
||||
override fun trace(msg: String) = logger.finest(msg)
|
||||
override fun warn(msg: String) = errorLogger.warning(msg)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.cli.logging.impl
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.StreamHandler
|
||||
|
||||
internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) {
|
||||
override fun publish(record: LogRecord) {
|
||||
super.publish(record)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.cli.patcher
|
||||
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.utils.filesystem.ZipFileSystemUtils
|
||||
import app.revanced.utils.patcher.addPatchesFiltered
|
||||
import app.revanced.utils.patcher.applyPatchesVerbose
|
||||
@@ -10,31 +11,38 @@ import java.nio.file.Files
|
||||
|
||||
internal object Patcher {
|
||||
internal fun start(patcher: app.revanced.patcher.Patcher, output: File) {
|
||||
val args = args.pArgs;
|
||||
val inputFile = args.inputFile
|
||||
val args = args.patchArgs?.patchingArgs!!
|
||||
|
||||
// merge files like necessary integrations
|
||||
patcher.mergeFiles()
|
||||
// add patches, but filter incompatible or excluded patches
|
||||
patcher.addPatchesFiltered(includeFilter = args.includedPatches.isNotEmpty())
|
||||
patcher.addPatchesFiltered()
|
||||
// apply patches
|
||||
patcher.applyPatchesVerbose()
|
||||
|
||||
// write output file
|
||||
if (output.exists()) Files.delete(output.toPath())
|
||||
args.inputFile.copyTo(output)
|
||||
inputFile.copyTo(output)
|
||||
|
||||
ZipFileSystemUtils(output).use { fileSystem ->
|
||||
val result = patcher.save()
|
||||
ZipFileSystemUtils(output).use { outputFileSystem ->
|
||||
// replace all dex files
|
||||
val result = patcher.save()
|
||||
result.dexFiles.forEach {
|
||||
fileSystem.write(it.name, it.memoryDataStore.data)
|
||||
logger.info("Writing dex file ${it.name}")
|
||||
outputFileSystem.write(it.name, it.dexFileInputStream.readAllBytes())
|
||||
}
|
||||
|
||||
// write resources
|
||||
if (!args.disableResourcePatching) {
|
||||
fileSystem.writePathRecursively(File(args.cacheDirectory).resolve("build").toPath())
|
||||
fileSystem.uncompress(*result.doNotCompress!!.toTypedArray())
|
||||
logger.info("Writing resources...")
|
||||
|
||||
ZipFileSystemUtils(result.resourceFile!!).use { resourceFileSystem ->
|
||||
val resourceFiles = resourceFileSystem.getFile(File.separator)
|
||||
outputFileSystem.writePathRecursively(resourceFiles)
|
||||
}
|
||||
}
|
||||
|
||||
result.doNotCompress?.let { outputFileSystem.uncompress(*it.toTypedArray()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.cli.patcher.logging.impl
|
||||
|
||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
||||
import java.util.logging.Logger
|
||||
|
||||
internal object PatcherLogger : app.revanced.patcher.logging.Logger{
|
||||
private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name))
|
||||
|
||||
override fun error(msg: String) = logger.error(msg)
|
||||
override fun info(msg: String) = logger.info(msg)
|
||||
override fun warn(msg: String)= logger.warn(msg)
|
||||
override fun trace(msg: String)= logger.trace(msg)
|
||||
}
|
||||
@@ -1,25 +1,12 @@
|
||||
package app.revanced.cli.signing
|
||||
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.utils.signing.Signer
|
||||
import app.revanced.utils.signing.align.ZipAligner
|
||||
import java.io.File
|
||||
|
||||
object Signing {
|
||||
fun start(inputFile: File, outputFile: File, cn: String, password: String) {
|
||||
val cacheDirectory = File(args.pArgs.cacheDirectory)
|
||||
val alignedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk")
|
||||
val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk")
|
||||
|
||||
// align the inputFile and write to alignedOutput
|
||||
ZipAligner.align(inputFile, alignedOutput)
|
||||
// sign the alignedOutput and write to signedOutput
|
||||
// the reason is, in case the signer fails
|
||||
// it does not damage the output file
|
||||
val keyStore = Signer(cn, password).signApk(alignedOutput, signedOutput)
|
||||
|
||||
// afterwards copy over the file and the keystore to the output
|
||||
signedOutput.copyTo(outputFile, true)
|
||||
keyStore.copyTo(outputFile.resolveSibling(keyStore.name), true)
|
||||
fun sign(alignedFile: File, signedOutput: File, signingOptions: SigningOptions) {
|
||||
logger.info("Signing ${alignedFile.name} to ${signedOutput.name}")
|
||||
Signer(signingOptions).signApk(alignedFile, signedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.revanced.cli.signing
|
||||
|
||||
data class SigningOptions(
|
||||
val cn: String,
|
||||
val password: String,
|
||||
val keyStoreFilePath: String
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.utils.adb
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.managers.PackageManager
|
||||
@@ -20,7 +21,7 @@ internal class Adb(
|
||||
?: throw IllegalArgumentException("No such device with name $deviceName")
|
||||
|
||||
if (!modeInstall && device.run("su -h", false) != 0)
|
||||
throw IllegalArgumentException("Root required on $deviceName. Deploying failed.")
|
||||
throw IllegalArgumentException("Root required on $deviceName. Task failed")
|
||||
}
|
||||
|
||||
private fun String.replacePlaceholder(with: String? = null): String {
|
||||
@@ -29,12 +30,16 @@ internal class Adb(
|
||||
|
||||
internal fun deploy() {
|
||||
if (modeInstall) {
|
||||
logger.info("Installing without mounting")
|
||||
|
||||
PackageManager(device).install(file)
|
||||
} else {
|
||||
logger.info("Installing by mounting")
|
||||
|
||||
// push patched file
|
||||
device.copy(Constants.PATH_INIT_PUSH, file)
|
||||
|
||||
// create revanced path
|
||||
// create revanced folder path
|
||||
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
|
||||
|
||||
// prepare mounting the apk
|
||||
@@ -48,16 +53,8 @@ internal class Adb(
|
||||
// install mount script
|
||||
device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder())
|
||||
|
||||
// push umount script
|
||||
device.createFile(
|
||||
Constants.PATH_INIT_PUSH,
|
||||
Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder()
|
||||
)
|
||||
// install mount script
|
||||
device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder())
|
||||
|
||||
// unmount the apk for sanity
|
||||
device.run(Constants.PATH_UMOUNT.replacePlaceholder())
|
||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
||||
// mount the apk
|
||||
device.run(Constants.PATH_MOUNT.replacePlaceholder())
|
||||
|
||||
@@ -69,6 +66,21 @@ internal class Adb(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun uninstall() {
|
||||
logger.info("Uninstalling by unmounting")
|
||||
|
||||
// unmount the apk
|
||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
||||
|
||||
// delete revanced app
|
||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder())
|
||||
|
||||
// delete mount script
|
||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder())
|
||||
|
||||
logger.info("Finished uninstalling")
|
||||
}
|
||||
|
||||
private fun log() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val pipe = if (logging) {
|
||||
@@ -91,11 +103,11 @@ internal class Adb(
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("An error occurred while monitoring state of app", e)
|
||||
throw RuntimeException("An error occurred while monitoring the state of app", e)
|
||||
}
|
||||
}
|
||||
println("App closed, continuing.")
|
||||
logger.info("Stopped logging because the app was closed")
|
||||
process.destroy()
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,31 +21,25 @@ internal object Constants {
|
||||
internal const val PATH_REVANCED = "/data/adb/revanced/"
|
||||
|
||||
// revanced apk path
|
||||
private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
||||
internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
||||
|
||||
// (un)mount script paths
|
||||
// delete command
|
||||
internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER"
|
||||
|
||||
// mount script path
|
||||
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
|
||||
internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT"
|
||||
|
||||
// move to revanced apk path & set permissions
|
||||
internal const val COMMAND_PREPARE_MOUNT_APK =
|
||||
"base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
|
||||
|
||||
// unmount command
|
||||
internal const val COMMAND_UMOUNT =
|
||||
"stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) && umount -l ${'$'}stock_path"
|
||||
|
||||
// install mount script & set permissions
|
||||
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
|
||||
|
||||
// install umount script & set permissions
|
||||
internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT"
|
||||
|
||||
// unmount script
|
||||
internal val CONTENT_UMOUNT_SCRIPT =
|
||||
"""
|
||||
#!/system/bin/sh
|
||||
|
||||
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
umount -l ${'$'}stock_path
|
||||
""".trimIndent()
|
||||
|
||||
// mount script
|
||||
internal val CONTENT_MOUNT_SCRIPT =
|
||||
"""
|
||||
|
||||
@@ -13,6 +13,10 @@ internal class ZipFileSystemUtils(
|
||||
private var zipFileSystem = FileSystems.newFileSystem(file.toPath(), mapOf("noCompression" to true))
|
||||
|
||||
private fun Path.deleteRecursively() {
|
||||
if (!Files.exists(this)) {
|
||||
throw IllegalStateException("File exists in real folder but not in zip file system")
|
||||
}
|
||||
|
||||
if (Files.isDirectory(this)) {
|
||||
Files.list(this).forEach { path ->
|
||||
path.deleteRecursively()
|
||||
@@ -22,17 +26,19 @@ internal class ZipFileSystemUtils(
|
||||
Files.delete(this)
|
||||
}
|
||||
|
||||
internal fun getFile(path: String) = zipFileSystem.getPath(path)
|
||||
|
||||
internal fun writePathRecursively(path: Path) {
|
||||
Files.list(path).let { fileStream ->
|
||||
Files.list(path).use { fileStream ->
|
||||
fileStream.forEach { filePath ->
|
||||
val fileSystemPath = filePath.getRelativePath(path)
|
||||
fileSystemPath.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
fileStream
|
||||
}.close()
|
||||
|
||||
Files.walk(path).let { fileStream ->
|
||||
Files.walk(path).use { fileStream ->
|
||||
// don't include build directory
|
||||
// by skipping the root node.
|
||||
fileStream.skip(1).forEach { filePath ->
|
||||
val relativePath = filePath.getRelativePath(path)
|
||||
|
||||
@@ -43,15 +49,14 @@ internal class ZipFileSystemUtils(
|
||||
|
||||
Files.copy(filePath, relativePath)
|
||||
}
|
||||
|
||||
fileStream
|
||||
}.close()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun write(path: String, content: ByteArray) = Files.write(zipFileSystem.getPath(path), content)
|
||||
|
||||
private fun Path.getRelativePath(path: Path): Path = zipFileSystem.getPath(path.relativize(this).toString())
|
||||
|
||||
// TODO: figure out why the file system is uncompressed by default and how to fix it
|
||||
internal fun uncompress(vararg paths: String) =
|
||||
paths.forEach { Files.setAttribute(zipFileSystem.getPath(it), "zip:method", ZipEntry.STORED) }
|
||||
|
||||
|
||||
@@ -2,55 +2,59 @@ package app.revanced.utils.patcher
|
||||
|
||||
import app.revanced.cli.command.MainCommand
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import app.revanced.patcher.extensions.PatchExtensions.include
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.patch.implementation.JarPatchBundle
|
||||
|
||||
fun Patcher.addPatchesFiltered(
|
||||
includeFilter: Boolean = false
|
||||
) {
|
||||
fun Patcher.addPatchesFiltered() {
|
||||
val packageName = this.data.packageMetadata.packageName
|
||||
val packageVersion = this.data.packageMetadata.packageVersion
|
||||
|
||||
MainCommand.args.patchBundles.forEach { bundle ->
|
||||
args.patchArgs?.patchBundles!!.forEach { bundle ->
|
||||
val includedPatches = mutableListOf<Class<out Patch<Data>>>()
|
||||
JarPatchBundle(bundle).loadPatches().forEach patch@{ patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
val patchName = patch.patchName
|
||||
|
||||
val prefix = "[skipped] $patchName"
|
||||
val prefix = "Skipping $patchName"
|
||||
|
||||
val args = MainCommand.args.pArgs
|
||||
val args = MainCommand.args.patchArgs?.patchingArgs!!
|
||||
|
||||
if (includeFilter) {
|
||||
if (!args.includedPatches.contains(patchName)) {
|
||||
println("$prefix: Explicitly excluded.")
|
||||
return@patch
|
||||
}
|
||||
} else if (!patch.include) {
|
||||
println("$prefix: Implicitly excluded.")
|
||||
if (args.excludedPatches.contains(patchName)) {
|
||||
logger.info("$prefix: Explicitly excluded")
|
||||
return@patch
|
||||
} else if ((!patch.include || args.defaultExclude) && !args.includedPatches.contains(patchName)) {
|
||||
logger.info("$prefix: Not explicitly included")
|
||||
return@patch
|
||||
}
|
||||
|
||||
if (compatiblePackages == null) println("$prefix: Missing compatibility annotation. Continuing.")
|
||||
if (compatiblePackages == null) logger.warn("$prefix: Missing compatibility annotation. Continuing.")
|
||||
else {
|
||||
if (!compatiblePackages.any { it.name == packageName }) {
|
||||
println("$prefix: Incompatible package.")
|
||||
logger.warn("$prefix: Incompatible with $packageName. This patch is only compatible with ${
|
||||
compatiblePackages.joinToString(
|
||||
", "
|
||||
) { it.name }
|
||||
}")
|
||||
return@patch
|
||||
}
|
||||
|
||||
if (!(args.debugging || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion }})) {
|
||||
println("$prefix: The package version is $packageVersion and is incompatible.")
|
||||
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
|
||||
val compatibleWith = compatiblePackages.map { _package ->
|
||||
"${_package.name}: ${_package.versions.joinToString(", ")}"
|
||||
}.joinToString(";")
|
||||
logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with version $compatibleWith")
|
||||
return@patch
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace("Adding $patchName")
|
||||
includedPatches.add(patch)
|
||||
println("[added] $patchName")
|
||||
}
|
||||
this.addPatches(includedPatches)
|
||||
}
|
||||
@@ -59,14 +63,16 @@ fun Patcher.addPatchesFiltered(
|
||||
fun Patcher.applyPatchesVerbose() {
|
||||
this.applyPatches().forEach { (patch, result) ->
|
||||
if (result.isSuccess) {
|
||||
println("[success] $patch")
|
||||
logger.info("$patch succeeded")
|
||||
return@forEach
|
||||
}
|
||||
println("[error] $patch:")
|
||||
logger.error("$patch failed:")
|
||||
result.exceptionOrNull()!!.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun Patcher.mergeFiles() {
|
||||
this.addFiles(MainCommand.args.pArgs.mergeFiles)
|
||||
this.addFiles(args.patchArgs?.patchingArgs!!.mergeFiles) { file ->
|
||||
logger.info("Merging $file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package app.revanced.utils.signature
|
||||
|
||||
import app.revanced.patcher.Patcher
|
||||
import org.jf.dexlib2.iface.Method
|
||||
|
||||
object Signature {
|
||||
|
||||
fun checkSignatures(patcher: Patcher) {
|
||||
TODO()
|
||||
/**
|
||||
val failed = mutableListOf<String>()
|
||||
for (signature in patcher.resolveSignatures()) {
|
||||
val signatureClass = signature::class.java
|
||||
val signatureName = signature.name ?: signatureClass.simpleName
|
||||
if (!signature.resolved) {
|
||||
failed.add(signatureName)
|
||||
continue
|
||||
}
|
||||
|
||||
val method = signature.result!!.method
|
||||
val matchingMethod = signature.matchingMethod ?: MatchingMethod()
|
||||
|
||||
println(
|
||||
"""
|
||||
[Signature] $signatureName
|
||||
[Method] ${matchingMethod.definingClass}->${matchingMethod.name}
|
||||
[Match] ${method.definingClass}->${method.toStr()}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
signature.fuzzyThreshold.let {
|
||||
val warnings = signature.result!!.scanResult.warnings!!
|
||||
println(
|
||||
"""
|
||||
[Warnings: ${warnings.count()}]
|
||||
${warnings.joinToString(separator = "\n") { warning -> "${warning.instructionIndex} / ${warning.patternIndex}: ${warning.wrongOpcode} (expected: ${warning.correctOpcode})" }}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
println(
|
||||
"""
|
||||
${"=".repeat(50)}
|
||||
[Failed signatures: ${failed.size}]
|
||||
${failed.joinToString(separator = "\n") { it }}
|
||||
""".trimIndent()
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
private fun Method.toStr(): String {
|
||||
return "${this.name}(${this.parameterTypes.joinToString("")})${this.returnType}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package app.revanced.utils.signing
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.cli.signing.SigningOptions
|
||||
import com.android.apksig.ApkSigner
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
@@ -17,9 +19,9 @@ import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
internal class Signer(
|
||||
private val cn: String, password: String
|
||||
private val signingOptions: SigningOptions
|
||||
) {
|
||||
private val passwordCharArray = password.toCharArray()
|
||||
private val passwordCharArray = signingOptions.password.toCharArray()
|
||||
private fun newKeystore(out: File) {
|
||||
val (publicKey, privateKey) = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
@@ -34,7 +36,7 @@ internal class Signer(
|
||||
val pair = gen.generateKeyPair()
|
||||
var serialNumber: BigInteger
|
||||
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
||||
val x500Name = X500Name("CN=$cn")
|
||||
val x500Name = X500Name("CN=${signingOptions.cn}")
|
||||
val builder = X509v3CertificateBuilder(
|
||||
x500Name,
|
||||
serialNumber,
|
||||
@@ -48,30 +50,30 @@ internal class Signer(
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||
}
|
||||
|
||||
fun signApk(input: File, output: File): File {
|
||||
fun signApk(input: File, output: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
// TODO: keystore should be saved securely
|
||||
val ks = File(input.parent, "${output.nameWithoutExtension}.keystore")
|
||||
if (!ks.exists()) newKeystore(ks)
|
||||
val ks = File(signingOptions.keyStoreFilePath)
|
||||
if (!ks.exists()) newKeystore(ks) else {
|
||||
logger.info("Found existing keystore: ${ks.name}")
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
|
||||
val config = ApkSigner.SignerConfig.Builder(
|
||||
cn,
|
||||
signingOptions.cn,
|
||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||
).build()
|
||||
|
||||
val signer = ApkSigner.Builder(listOf(config))
|
||||
signer.setCreatedBy(cn)
|
||||
signer.setCreatedBy(signingOptions.cn)
|
||||
signer.setInputApk(input)
|
||||
signer.setOutputApk(output)
|
||||
|
||||
signer.build().sign()
|
||||
|
||||
return ks
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,28 @@
|
||||
package app.revanced.utils.signing.align
|
||||
|
||||
import app.revanced.utils.signing.align.stream.MultiOutputStream
|
||||
import app.revanced.utils.signing.align.stream.PeekingFakeStream
|
||||
import java.io.BufferedOutputStream
|
||||
import app.revanced.utils.signing.align.zip.ZipFile
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
internal object ZipAligner {
|
||||
fun align(input: File, output: File, alignment: Int = 4) {
|
||||
val zipFile = ZipFile(input)
|
||||
private const val DEFAULT_ALIGNMENT = 4
|
||||
private const val LIBRARY_ALIGNMENT = 4096
|
||||
|
||||
val entries: Enumeration<out ZipEntry?> = zipFile.entries()
|
||||
fun align(input: File, output: File) {
|
||||
val inputZip = ZipFile(input)
|
||||
val outputZip = ZipFile(output)
|
||||
|
||||
// fake
|
||||
val peekingFakeStream = PeekingFakeStream()
|
||||
val fakeOutputStream = ZipOutputStream(peekingFakeStream)
|
||||
// real
|
||||
val zipOutputStream = ZipOutputStream(BufferedOutputStream(output.outputStream()))
|
||||
for (entry in inputZip.entries) {
|
||||
val data = inputZip.getDataForEntry(entry)
|
||||
|
||||
val multiOutputStream = MultiOutputStream(
|
||||
listOf(
|
||||
fakeOutputStream, // fake, used to add the data to the fake stream
|
||||
zipOutputStream // real
|
||||
)
|
||||
)
|
||||
if (entry.compression == 0.toUShort()) {
|
||||
val alignment = if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
|
||||
|
||||
var bias = 0
|
||||
while (entries.hasMoreElements()) {
|
||||
var padding = 0
|
||||
|
||||
val entry: ZipEntry = entries.nextElement()!!
|
||||
// fake, used to calculate the file offset of the entry
|
||||
fakeOutputStream.putNextEntry(entry)
|
||||
|
||||
if (entry.size == entry.compressedSize) {
|
||||
val fileOffset = peekingFakeStream.peek()
|
||||
val newOffset = fileOffset + bias
|
||||
padding = ((alignment - (newOffset % alignment)) % alignment).toInt()
|
||||
|
||||
// real
|
||||
entry.extra = if (entry.extra == null) ByteArray(padding)
|
||||
else Arrays.copyOf(entry.extra, entry.extra.size + padding)
|
||||
outputZip.addEntryAligned(entry, data, alignment)
|
||||
} else {
|
||||
outputZip.addEntry(entry, data)
|
||||
}
|
||||
|
||||
zipOutputStream.putNextEntry(entry)
|
||||
zipFile.getInputStream(entry).copyTo(multiOutputStream)
|
||||
|
||||
// fake, used to add remaining bytes
|
||||
fakeOutputStream.closeEntry()
|
||||
// real
|
||||
zipOutputStream.closeEntry()
|
||||
|
||||
bias += padding
|
||||
}
|
||||
|
||||
zipFile.close()
|
||||
zipOutputStream.close()
|
||||
outputZip.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package app.revanced.utils.signing.align.stream
|
||||
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class MultiOutputStream(
|
||||
private val streams: Iterable<OutputStream>,
|
||||
) : OutputStream() {
|
||||
override fun write(b: ByteArray, off: Int, len: Int) = streams.forEach {
|
||||
it.write(b, off, len)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray) = streams.forEach {
|
||||
it.write(b)
|
||||
}
|
||||
|
||||
override fun write(b: Int) = streams.forEach {
|
||||
it.write(b)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package app.revanced.utils.signing.align.stream
|
||||
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class PeekingFakeStream : OutputStream() {
|
||||
private var numberOfBytes: Long = 0
|
||||
|
||||
fun peek() = numberOfBytes
|
||||
|
||||
override fun write(b: Int) {
|
||||
numberOfBytes++
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray) {
|
||||
numberOfBytes += b.size
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, offset: Int, len: Int) {
|
||||
numberOfBytes += len
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun UInt.toLittleEndian() =
|
||||
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
|
||||
|
||||
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
|
||||
|
||||
fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
|
||||
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
|
||||
|
||||
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
|
||||
|
||||
fun ByteBuffer.getUShort() = this.getShort().toUShort()
|
||||
fun ByteBuffer.getUInt() = this.getInt().toUInt()
|
||||
|
||||
fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
|
||||
fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShort() = this.readShort().toUShort()
|
||||
fun DataInput.readUInt() = this.readInt().toUInt()
|
||||
|
||||
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
|
||||
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
|
||||
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
|
||||
|
||||
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
|
||||
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())
|
||||
130
src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt
Normal file
130
src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEndRecord
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
class ZipFile(val file: File) {
|
||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
||||
|
||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
||||
|
||||
init {
|
||||
//if file isn't empty try to load entries
|
||||
if (file.length() > 0) {
|
||||
val endRecord = findEndRecord()
|
||||
|
||||
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
|
||||
throw IllegalArgumentException("Multi-file archives are not supported")
|
||||
|
||||
entries = readEntries(endRecord).toMutableList()
|
||||
}
|
||||
|
||||
//seek back to start for writing
|
||||
filePointer.seek(0)
|
||||
}
|
||||
|
||||
private fun findEndRecord(): ZipEndRecord {
|
||||
//look from end to start since end record is at the end
|
||||
for (i in filePointer.length() - 1 downTo 0) {
|
||||
filePointer.seek(i)
|
||||
//possible beginning of signature
|
||||
if (filePointer.readByte() == 0x50.toByte()) {
|
||||
//seek back to get the full int
|
||||
filePointer.seek(i)
|
||||
val possibleSignature = filePointer.readUIntLE()
|
||||
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
||||
filePointer.seek(i)
|
||||
return ZipEndRecord.fromECD(filePointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception("Couldn't find end record")
|
||||
}
|
||||
|
||||
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
|
||||
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
|
||||
|
||||
val numberOfEntries = endRecord.diskEntries.toInt()
|
||||
|
||||
return buildList(numberOfEntries) {
|
||||
for (i in 1..numberOfEntries) {
|
||||
add(ZipEntry.fromCDE(filePointer).also
|
||||
{
|
||||
//for some reason the local extra field can be different from the central one
|
||||
it.readLocalExtra(
|
||||
filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
it.localHeaderOffset.toLong() + 28,
|
||||
2
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeCDE() {
|
||||
val CDEStart = filePointer.channel.position().toUInt()
|
||||
|
||||
entries.forEach {
|
||||
filePointer.channel.write(it.toCDE())
|
||||
}
|
||||
|
||||
val entriesCount = entries.size.toUShort()
|
||||
|
||||
val endRecord = ZipEndRecord(
|
||||
0u,
|
||||
0u,
|
||||
entriesCount,
|
||||
entriesCount,
|
||||
filePointer.channel.position().toUInt() - CDEStart,
|
||||
CDEStart,
|
||||
""
|
||||
)
|
||||
|
||||
filePointer.channel.write(endRecord.toECD())
|
||||
}
|
||||
|
||||
fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
||||
|
||||
filePointer.channel.write(entry.toLFH())
|
||||
filePointer.channel.write(data)
|
||||
|
||||
entries.add(entry)
|
||||
}
|
||||
|
||||
fun addEntryAligned(entry: ZipEntry, data: ByteBuffer, alignment: Int) {
|
||||
//calculate where data would end up
|
||||
val dataOffset = filePointer.filePointer + entry.LFHSize
|
||||
|
||||
val mod = dataOffset % alignment
|
||||
|
||||
//wrong alignment
|
||||
if (mod != 0L) {
|
||||
//add padding at end of extra field
|
||||
entry.localExtraField =
|
||||
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
||||
}
|
||||
|
||||
addEntry(entry, data)
|
||||
}
|
||||
|
||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||
return filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
entry.dataOffset.toLong(),
|
||||
entry.compressedSize.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
writeCDE()
|
||||
filePointer.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package app.revanced.utils.signing.align.zip.structures
|
||||
|
||||
import app.revanced.utils.signing.align.zip.putUInt
|
||||
import app.revanced.utils.signing.align.zip.putUShort
|
||||
import app.revanced.utils.signing.align.zip.readUIntLE
|
||||
import app.revanced.utils.signing.align.zip.readUShortLE
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
data class ZipEndRecord(
|
||||
val diskNumber: UShort,
|
||||
val startingDiskNumber: UShort,
|
||||
val diskEntries: UShort,
|
||||
val totalEntries: UShort,
|
||||
val centralDirectorySize: UInt,
|
||||
val centralDirectoryStartOffset: UInt,
|
||||
val fileComment: String,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val ECD_HEADER_SIZE = 22
|
||||
const val ECD_SIGNATURE = 0x06054b50u
|
||||
|
||||
fun fromECD(input: DataInput): ZipEndRecord {
|
||||
val signature = input.readUIntLE()
|
||||
|
||||
if (signature != ECD_SIGNATURE)
|
||||
throw IllegalArgumentException("Input doesn't start with end record signature")
|
||||
|
||||
val diskNumber = input.readUShortLE()
|
||||
val startingDiskNumber = input.readUShortLE()
|
||||
val diskEntries = input.readUShortLE()
|
||||
val totalEntries = input.readUShortLE()
|
||||
val centralDirectorySize = input.readUIntLE()
|
||||
val centralDirectoryStartOffset = input.readUIntLE()
|
||||
val fileCommentLength = input.readUShortLE()
|
||||
var fileComment = ""
|
||||
|
||||
if (fileCommentLength > 0u) {
|
||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
||||
input.readFully(fileCommentBytes)
|
||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
return ZipEndRecord(
|
||||
diskNumber,
|
||||
startingDiskNumber,
|
||||
diskEntries,
|
||||
totalEntries,
|
||||
centralDirectorySize,
|
||||
centralDirectoryStartOffset,
|
||||
fileComment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toECD(): ByteBuffer {
|
||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(ECD_SIGNATURE)
|
||||
buffer.putUShort(diskNumber)
|
||||
buffer.putUShort(startingDiskNumber)
|
||||
buffer.putUShort(diskEntries)
|
||||
buffer.putUShort(totalEntries)
|
||||
buffer.putUInt(centralDirectorySize)
|
||||
buffer.putUInt(centralDirectoryStartOffset)
|
||||
buffer.putUShort(commentBytes.size.toUShort())
|
||||
|
||||
buffer.put(commentBytes)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package app.revanced.utils.signing.align.zip.structures
|
||||
|
||||
import app.revanced.utils.signing.align.zip.getUShort
|
||||
import app.revanced.utils.signing.align.zip.putUInt
|
||||
import app.revanced.utils.signing.align.zip.putUShort
|
||||
import app.revanced.utils.signing.align.zip.readUIntLE
|
||||
import app.revanced.utils.signing.align.zip.readUShortLE
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
data class ZipEntry(
|
||||
val version: UShort,
|
||||
val versionNeeded: UShort,
|
||||
val flags: UShort,
|
||||
val compression: UShort,
|
||||
val modificationTime: UShort,
|
||||
val modificationDate: UShort,
|
||||
val crc32: UInt,
|
||||
val compressedSize: UInt,
|
||||
val uncompressedSize: UInt,
|
||||
val diskNumber: UShort,
|
||||
val internalAttributes: UShort,
|
||||
val externalAttributes: UInt,
|
||||
var localHeaderOffset: UInt,
|
||||
val fileName: String,
|
||||
val extraField: ByteArray,
|
||||
val fileComment: String,
|
||||
var localExtraField: ByteArray = ByteArray(0), //seperate for alignment
|
||||
) {
|
||||
val LFHSize: Int
|
||||
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
|
||||
|
||||
val dataOffset: UInt
|
||||
get() = localHeaderOffset + LFHSize.toUInt()
|
||||
|
||||
companion object {
|
||||
const val CDE_HEADER_SIZE = 46
|
||||
const val CDE_SIGNATURE = 0x02014b50u
|
||||
|
||||
const val LFH_HEADER_SIZE = 30
|
||||
const val LFH_SIGNATURE = 0x04034b50u
|
||||
|
||||
fun fromCDE(input: DataInput): ZipEntry {
|
||||
val signature = input.readUIntLE()
|
||||
|
||||
if (signature != CDE_SIGNATURE)
|
||||
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
|
||||
|
||||
val version = input.readUShortLE()
|
||||
val versionNeeded = input.readUShortLE()
|
||||
var flags = input.readUShortLE()
|
||||
val compression = input.readUShortLE()
|
||||
val modificationTime = input.readUShortLE()
|
||||
val modificationDate = input.readUShortLE()
|
||||
val crc32 = input.readUIntLE()
|
||||
val compressedSize = input.readUIntLE()
|
||||
val uncompressedSize = input.readUIntLE()
|
||||
val fileNameLength = input.readUShortLE()
|
||||
var fileName = ""
|
||||
val extraFieldLength = input.readUShortLE()
|
||||
var extraField = ByteArray(extraFieldLength.toInt())
|
||||
val fileCommentLength = input.readUShortLE()
|
||||
var fileComment = ""
|
||||
val diskNumber = input.readUShortLE()
|
||||
val internalAttributes = input.readUShortLE()
|
||||
val externalAttributes = input.readUIntLE()
|
||||
val localHeaderOffset = input.readUIntLE()
|
||||
|
||||
val variableFieldsLength = fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
|
||||
|
||||
if (variableFieldsLength > 0) {
|
||||
val fileNameBytes = ByteArray(fileNameLength.toInt())
|
||||
input.readFully(fileNameBytes)
|
||||
fileName = fileNameBytes.toString(Charsets.UTF_8)
|
||||
|
||||
input.readFully(extraField)
|
||||
|
||||
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
|
||||
input.readFully(fileCommentBytes)
|
||||
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
flags = (flags and 0b1000u.inv().toUShort()) //disable data descriptor flag as they are not used
|
||||
|
||||
return ZipEntry(
|
||||
version,
|
||||
versionNeeded,
|
||||
flags,
|
||||
compression,
|
||||
modificationTime,
|
||||
modificationDate,
|
||||
crc32,
|
||||
compressedSize,
|
||||
uncompressedSize,
|
||||
diskNumber,
|
||||
internalAttributes,
|
||||
externalAttributes,
|
||||
localHeaderOffset,
|
||||
fileName,
|
||||
extraField,
|
||||
fileComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun readLocalExtra(buffer: ByteBuffer) {
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
localExtraField = ByteArray(buffer.getUShort().toInt())
|
||||
}
|
||||
|
||||
fun toLFH(): ByteBuffer {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
|
||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(LFH_SIGNATURE)
|
||||
buffer.putUShort(versionNeeded)
|
||||
buffer.putUShort(flags)
|
||||
buffer.putUShort(compression)
|
||||
buffer.putUShort(modificationTime)
|
||||
buffer.putUShort(modificationDate)
|
||||
buffer.putUInt(crc32)
|
||||
buffer.putUInt(compressedSize)
|
||||
buffer.putUInt(uncompressedSize)
|
||||
buffer.putUShort(nameBytes.size.toUShort())
|
||||
buffer.putUShort(localExtraField.size.toUShort())
|
||||
|
||||
buffer.put(nameBytes)
|
||||
buffer.put(localExtraField)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
|
||||
fun toCDE(): ByteBuffer {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
|
||||
|
||||
val buffer = ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
|
||||
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
|
||||
|
||||
buffer.putUInt(CDE_SIGNATURE)
|
||||
buffer.putUShort(version)
|
||||
buffer.putUShort(versionNeeded)
|
||||
buffer.putUShort(flags)
|
||||
buffer.putUShort(compression)
|
||||
buffer.putUShort(modificationTime)
|
||||
buffer.putUShort(modificationDate)
|
||||
buffer.putUInt(crc32)
|
||||
buffer.putUInt(compressedSize)
|
||||
buffer.putUInt(uncompressedSize)
|
||||
buffer.putUShort(nameBytes.size.toUShort())
|
||||
buffer.putUShort(extraField.size.toUShort())
|
||||
buffer.putUShort(commentBytes.size.toUShort())
|
||||
buffer.putUShort(diskNumber)
|
||||
buffer.putUShort(internalAttributes)
|
||||
buffer.putUInt(externalAttributes)
|
||||
buffer.putUInt(localHeaderOffset)
|
||||
|
||||
buffer.put(nameBytes)
|
||||
buffer.put(extraField)
|
||||
buffer.put(commentBytes)
|
||||
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user