feat: add installer and enable app selection from storage (#2)

This commit is contained in:
Alberto Ponces
2022-08-13 10:56:30 +01:00
committed by GitHub
parent a00e94d2fe
commit e4f9b04de0
37 changed files with 1578 additions and 257 deletions

View File

@@ -4,6 +4,10 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:label="ReVanced Manager"
android:name="${applicationName}"
@@ -29,5 +33,15 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,13 @@
package app.revanced.manager
import androidx.annotation.NonNull
import app.revanced.manager.utils.Aapt
import app.revanced.manager.utils.aligning.ZipAligner
import app.revanced.manager.utils.signing.Signer
import app.revanced.manager.utils.zip.ZipFile
import app.revanced.manager.utils.zip.structures.ZipEntry
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.data.Data
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.description
@@ -12,20 +19,26 @@ import dalvik.system.DexClassLoader
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class MainActivity : FlutterActivity() {
private val CHANNEL = "app.revanced/patcher"
private val CHANNEL = "app.revanced.manager/patcher"
private var patches = mutableListOf<Class<out Patch<Data>>>()
private val tag = "Patcher"
private lateinit var methodChannel: MethodChannel
private lateinit var patcher: Patcher
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"loadPatches" -> {
val pathBundlesPaths = call.argument<List<String>>("pathBundlesPaths")
if (pathBundlesPaths != null) {
loadPatches(pathBundlesPaths)
result.success("OK")
result.success(loadPatches(pathBundlesPaths))
} else {
result.notImplemented()
}
@@ -36,7 +49,61 @@ class MainActivity : FlutterActivity() {
val targetVersion = call.argument<String>("targetVersion")
val ignoreVersion = call.argument<Boolean>("ignoreVersion")
if (targetPackage != null && targetVersion != null && ignoreVersion != null) {
result.success(getFilteredPatches(targetPackage, targetVersion, ignoreVersion))
result.success(
getFilteredPatches(targetPackage, targetVersion, ignoreVersion)
)
} else {
result.notImplemented()
}
}
"copyInputFile" -> {
val originalFilePath = call.argument<String>("originalFilePath")
val inputFilePath = call.argument<String>("inputFilePath")
if (originalFilePath != null && inputFilePath != null) {
result.success(copyInputFile(originalFilePath, inputFilePath))
} else {
result.notImplemented()
}
}
"createPatcher" -> {
val inputFilePath = call.argument<String>("inputFilePath")
val cacheDirPath = call.argument<String>("cacheDirPath")
if (inputFilePath != null && cacheDirPath != null) {
result.success(createPatcher(inputFilePath, cacheDirPath))
} else {
result.notImplemented()
}
}
"mergeIntegrations" -> {
val integrationsPath = call.argument<String>("integrationsPath")
if (integrationsPath != null) {
result.success(mergeIntegrations(integrationsPath))
} else {
result.notImplemented()
}
}
"applyPatches" -> {
val selectedPatches = call.argument<List<String>>("selectedPatches")
if (selectedPatches != null) {
result.success(applyPatches(selectedPatches))
} else {
result.notImplemented()
}
}
"repackPatchedFile" -> {
val inputFilePath = call.argument<String>("inputFilePath")
val patchedFilePath = call.argument<String>("patchedFilePath")
if (inputFilePath != null && patchedFilePath != null) {
result.success(repackPatchedFile(inputFilePath, patchedFilePath))
} else {
result.notImplemented()
}
}
"signPatchedFile" -> {
val patchedFilePath = call.argument<String>("patchedFilePath")
val outFilePath = call.argument<String>("outFilePath")
if (patchedFilePath != null && outFilePath != null) {
result.success(signPatchedFile(patchedFilePath, outFilePath))
} else {
result.notImplemented()
}
@@ -46,42 +113,126 @@ class MainActivity : FlutterActivity() {
}
}
fun loadPatches(pathBundlesPaths: List<String>) {
pathBundlesPaths.forEach { path ->
patches.addAll(DexPatchBundle(
path, DexClassLoader(
path,
context.cacheDir.path,
null,
javaClass.classLoader
fun loadPatches(pathBundlesPaths: List<String>): Boolean {
try {
pathBundlesPaths.forEach { path ->
patches.addAll(
DexPatchBundle(
path,
DexClassLoader(
path,
context.cacheDir.path,
null,
javaClass.classLoader
)
)
.loadPatches()
)
).loadPatches())
}
} catch (e: Exception) {
return false
}
return true
}
fun getCompatiblePackages(): List<String> {
val filteredPackages = mutableListOf<String>()
patches.forEach patch@{ patch ->
patch.compatiblePackages?.forEach { pkg ->
filteredPackages.add(pkg.name)
}
patch.compatiblePackages?.forEach { pkg -> filteredPackages.add(pkg.name) }
}
return filteredPackages.distinct()
}
fun getFilteredPatches(targetPackage: String, targetVersion: String, ignoreVersion: Boolean): List<Map<String, String?>> {
fun getFilteredPatches(
targetPackage: String,
targetVersion: String,
ignoreVersion: Boolean
): List<Map<String, String?>> {
val filteredPatches = mutableListOf<Map<String, String?>>()
patches.forEach patch@{ patch ->
patch.compatiblePackages?.forEach { pkg ->
if (pkg.name == targetPackage && (ignoreVersion || pkg.versions.isNotEmpty() || pkg.versions.contains(targetVersion))) {
var p = mutableMapOf<String, String?>();
p.put("name", patch.patchName);
p.put("version", patch.version);
p.put("description", patch.description);
if (pkg.name == targetPackage &&
(ignoreVersion ||
pkg.versions.isNotEmpty() ||
pkg.versions.contains(targetVersion))
) {
var p = mutableMapOf<String, String?>()
p.put("name", patch.patchName)
p.put("version", patch.version)
p.put("description", patch.description)
filteredPatches.add(p)
}
}
}
return filteredPatches
}
private fun findPatchesByIds(ids: Iterable<String>): List<Class<out Patch<Data>>> {
return patches.filter { patch -> ids.any { it == patch.patchName } }
}
fun copyInputFile(originalFilePath: String, inputFilePath: String): Boolean {
val originalFile = File(originalFilePath)
val inputFile = File(inputFilePath)
Files.copy(originalFile.toPath(), inputFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
return true
}
fun createPatcher(inputFilePath: String, cacheDirPath: String): Boolean {
val inputFile = File(inputFilePath)
val aaptPath = Aapt.binary(context).absolutePath
patcher = Patcher(PatcherOptions(inputFile, cacheDirPath, true, aaptPath, cacheDirPath))
return true
}
fun mergeIntegrations(integrationsPath: String): Boolean {
val integrations = File(integrationsPath)
if (patcher == null) return false
patcher.addFiles(listOf(integrations)) {}
return true
}
fun applyPatches(selectedPatches: List<String>): Boolean {
val patches = findPatchesByIds(selectedPatches)
if (patches.isEmpty()) return false
if (patcher == null) return false
patcher.addPatches(patches)
patcher.applyPatches().forEach { (patch, result) ->
if (result.isSuccess) {
val msg = "[success] $patch"
methodChannel.invokeMethod("updateInstallerLog", msg)
return@forEach
}
val msg = "[error] $patch:" + result.exceptionOrNull()!!
methodChannel.invokeMethod("updateInstallerLog", msg)
}
return true
}
fun repackPatchedFile(inputFilePath: String, patchedFilePath: String): Boolean {
val inputFile = File(inputFilePath)
val patchedFile = File(patchedFilePath)
if (patcher == null) return false
val result = patcher.save()
ZipFile(patchedFile).use { file ->
result.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.dexFileInputStream.readBytes()
)
}
result.resourceFile?.let {
file.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment)
}
file.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment)
}
return true
}
fun signPatchedFile(patchedFilePath: String, outFilePath: String): Boolean {
val patchedFile = File(patchedFilePath)
val outFile = File(outFilePath)
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile)
return true
}
}

View File

@@ -0,0 +1,12 @@
package app.revanced.manager.utils
import android.content.Context
import java.io.File
object Aapt {
fun binary(context: Context): File {
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
}
}
private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory }!!.first())

View File

@@ -0,0 +1,11 @@
package app.revanced.manager.utils.aligning
import app.revanced.manager.utils.zip.structures.ZipEntry
internal object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
fun getEntryAlignment(entry: ZipEntry): Int? =
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
}

View File

@@ -0,0 +1,75 @@
package app.revanced.manager.utils.signing
import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.math.BigInteger
import java.security.*
import java.security.cert.X509Certificate
import java.util.*
internal class Signer(
private val cn: String, password: String
) {
private val passwordCharArray = password.toCharArray()
private fun newKeystore(out: File) {
val (publicKey, privateKey) = createKey()
val privateKS = KeyStore.getInstance("BKS", "BC")
privateKS.load(null, passwordCharArray)
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
privateKS.store(FileOutputStream(out), passwordCharArray)
}
private fun createKey(): Pair<X509Certificate, PrivateKey> {
val gen = KeyPairGenerator.getInstance("RSA")
gen.initialize(2048)
val pair = gen.generateKeyPair()
var serialNumber: BigInteger
do serialNumber =
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
val x500Name = X500Name("CN=$cn")
val builder = X509v3CertificateBuilder(
x500Name,
serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
Locale.ENGLISH,
x500Name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
}
fun signApk(input: File, output: File) {
Security.addProvider(BouncyCastleProvider())
val ks = File(input.parent, "revanced-cli.keystore")
if (!ks.exists()) newKeystore(ks)
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,
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate)
).build()
val signer = ApkSigner.Builder(listOf(config))
signer.setCreatedBy(cn)
signer.setInputApk(input)
signer.setOutputApk(output)
signer.build().sign()
}
}

View File

@@ -0,0 +1,33 @@
package app.revanced.manager.utils.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.short.toUShort()
fun ByteBuffer.getUInt() = this.int.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())

View File

@@ -0,0 +1,176 @@
package app.revanced.manager.utils.zip
import app.revanced.manager.utils.zip.structures.ZipEndRecord
import app.revanced.manager.utils.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.zip.CRC32
import java.util.zip.Deflater
class ZipFile(val file: File) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
private var CDNeedsRewrite = false
private val compressionLevel = 5
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 writeCD() {
val CDStart = 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() - CDStart,
CDStart,
""
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
CDNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt()
filePointer.channel.write(entry.toLFH())
filePointer.channel.write(data)
entries.add(entry)
}
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
val uncompressedSize = data.size
val compressedData =
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
val compressedDataLength = compressor.deflate(compressedData)
val compressedBuffer =
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
compressor.end()
val crc = CRC32()
crc.update(data)
entry.compression = 8u //deflate compression
entry.uncompressedSize = uncompressedSize.toUInt()
entry.compressedSize = compressedDataLength.toUInt()
entry.crc32 = crc.value.toUInt()
addEntry(entry, compressedBuffer)
}
fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
alignment?.let { alignment ->
//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 copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry))
}
}
override fun close() {
if (CDNeedsRewrite) writeCD()
filePointer.close()
}
}

View File

@@ -0,0 +1,78 @@
package app.revanced.manager.utils.zip.structures
import app.revanced.manager.utils.zip.putUInt
import app.revanced.manager.utils.zip.putUShort
import app.revanced.manager.utils.zip.readUIntLE
import app.revanced.manager.utils.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
}
}

View File

@@ -0,0 +1,190 @@
package app.revanced.manager.utils.zip.structures
import app.revanced.manager.utils.zip.*
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEntry(
val version: UShort,
val versionNeeded: UShort,
val flags: UShort,
var compression: UShort,
val modificationTime: UShort,
val modificationDate: UShort,
var crc32: UInt,
var compressedSize: UInt,
var 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), //separate 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 createWithName(fileName: String): ZipEntry {
return ZipEntry(
0x1403u, //made by unix, version 20
0u,
0u,
0u,
0x0821u, //seems to be static time google uses, no idea
0x0221u, //same as above
0u,
0u,
0u,
0u,
0u,
0u,
0u,
fileName,
ByteArray(0),
""
)
}
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
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="." />
</paths>