mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-04-28 12:03:09 -07:00
app: add local file supports for HttpFileChannel
This commit is contained in:
@@ -1,28 +1,24 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.HttpFileChannel
|
||||
import okio.buffer
|
||||
import okio.inflate
|
||||
import okio.sink
|
||||
import com.topjohnwu.magisk.core.utils.DataSourceChannel
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.apache.commons.compress.archivers.zip.ZipMethod
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.zip.Inflater
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
class ExtractImage(
|
||||
private val url: String,
|
||||
private val outFile: File,
|
||||
private val console: MutableList<String>,
|
||||
private val logs: MutableList<String>,
|
||||
) {
|
||||
@Throws(IOException::class)
|
||||
fun start(outFile: File) {
|
||||
logs.add("Downloading from: $url")
|
||||
|
||||
val channel = HttpFileChannel(ServiceLocator.okhttp, url)
|
||||
fun consume(channel: DataSourceChannel) {
|
||||
ZipFile.builder()
|
||||
.setSeekableByteChannel(channel)
|
||||
.setIgnoreLocalFileHeader(true)
|
||||
@@ -55,7 +51,7 @@ class ExtractImage(
|
||||
@Throws(IOException::class)
|
||||
private fun extractFromOTAPackage(
|
||||
payload: ZipArchiveEntry,
|
||||
channel: HttpFileChannel,
|
||||
channel: DataSourceChannel,
|
||||
outFile: File,
|
||||
) {
|
||||
if (payload.method != ZipMethod.STORED.code) {
|
||||
@@ -68,7 +64,11 @@ class ExtractImage(
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun extractFromFactoryImage(zipFile: ZipFile, channel: HttpFileChannel, outFile: File) {
|
||||
private fun extractFromFactoryImage(
|
||||
zipFile: ZipFile,
|
||||
channel: DataSourceChannel,
|
||||
outFile: File
|
||||
) {
|
||||
console.add("- Processing as factory image package")
|
||||
|
||||
findBootImageZipEntry(zipFile)?.let { entry ->
|
||||
@@ -88,14 +88,17 @@ class ExtractImage(
|
||||
}
|
||||
|
||||
private fun findBootImageZipEntry(zipFile: ZipFile): ZipArchiveEntry? {
|
||||
return zipFile.entries.asSequence().find { it.name == "init_boot.img" }
|
||||
?: zipFile.entries.asSequence().find { it.name == "boot.img" }
|
||||
return zipFile.entries.asSequence().find {
|
||||
it.name.substringAfterLast('/') == "init_boot.img"
|
||||
} ?: zipFile.entries.asSequence().find {
|
||||
it.name.substringAfterLast('/') == "boot.img"
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun extractFromInnerImageZip(
|
||||
entry: ZipArchiveEntry,
|
||||
channel: HttpFileChannel,
|
||||
channel: DataSourceChannel,
|
||||
outFile: File
|
||||
) {
|
||||
logs.add("Found inner image ZIP: ${entry.name}")
|
||||
@@ -120,7 +123,7 @@ class ExtractImage(
|
||||
private fun extractImageFile(
|
||||
zipFile: ZipFile,
|
||||
entry: ZipArchiveEntry,
|
||||
channel: HttpFileChannel,
|
||||
channel: DataSourceChannel,
|
||||
outFile: File,
|
||||
) {
|
||||
console.add("- Found boot image entry: ${entry.name} (${entry.size} bytes)")
|
||||
@@ -143,9 +146,13 @@ class ExtractImage(
|
||||
}
|
||||
|
||||
ZipMethod.DEFLATED.code -> {
|
||||
channel.streamRead(entry.dataOffset, entry.size).inflate().use { source ->
|
||||
outFile.sink().buffer().use { sink ->
|
||||
sink.writeAll(source)
|
||||
InflaterInputStream(
|
||||
channel.streamRead(entry.dataOffset, entry.size),
|
||||
Inflater(true),
|
||||
16 * 1024
|
||||
).use { input ->
|
||||
FileOutputStream(outFile).use { out ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ package com.topjohnwu.magisk.core.tasks
|
||||
import android.net.Uri
|
||||
import android.os.FileUtils
|
||||
import android.os.Process
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.O_WRONLY
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
@@ -20,6 +17,7 @@ import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.DataSourceChannel
|
||||
import com.topjohnwu.magisk.core.utils.DummyList
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
@@ -36,8 +34,6 @@ import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream
|
||||
import timber.log.Timber
|
||||
@@ -329,108 +325,6 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private suspend fun processZip(zipIn: ZipArchiveInputStream): ExtendedFile {
|
||||
console.add("- Processing zip file")
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
var entry: ZipArchiveEntry
|
||||
while (true) {
|
||||
entry = zipIn.nextEntry ?: break
|
||||
if (entry.isDirectory) continue
|
||||
when (entry.name.substringAfterLast('/')) {
|
||||
"payload.bin" -> {
|
||||
try {
|
||||
return processPayload(zipIn)
|
||||
} catch (e: IOException) {
|
||||
// No boot image in payload.bin, continue to find boot images
|
||||
}
|
||||
}
|
||||
"init_boot.img" -> {
|
||||
console.add("- Extracting init_boot.img")
|
||||
zipIn.copyAndCloseOut(initBoot.newOutputStream())
|
||||
return initBoot
|
||||
}
|
||||
"boot.img" -> {
|
||||
console.add("- Extracting boot.img")
|
||||
zipIn.copyAndCloseOut(boot.newOutputStream())
|
||||
// Don't return here since there might be an init_boot.img
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boot.exists()) {
|
||||
return boot
|
||||
} else {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processPayload(input: InputStream): ExtendedFile {
|
||||
var fifo: File? = null
|
||||
try {
|
||||
console.add("- Processing payload.bin")
|
||||
fifo = File.createTempFile("payload-fifo-", null, installDir)
|
||||
fifo.delete()
|
||||
Os.mkfifo(fifo.path, 420 /* 0644 */)
|
||||
|
||||
// Enqueue the shell command first, or the subsequent FIFO open will block
|
||||
val future = arrayOf(
|
||||
"cd $installDir",
|
||||
"./magiskboot extract $fifo",
|
||||
"cd /"
|
||||
).eq()
|
||||
|
||||
val fd = Os.open(fifo.path, O_WRONLY, 0)
|
||||
try {
|
||||
val bufSize = 1024 * 1024
|
||||
val buf = ByteBuffer.allocate(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
while (buf.hasRemaining()) {
|
||||
try {
|
||||
Os.write(fd, buf)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.EPIPE)
|
||||
throw e
|
||||
// If SIGPIPE, then the other side is closed, we're done
|
||||
break
|
||||
}
|
||||
if (!buf.hasRemaining()) {
|
||||
buf.limit(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Os.close(fd)
|
||||
}
|
||||
|
||||
val success = try { future.get().isSuccess } catch (e: Exception) { false }
|
||||
if (!success) {
|
||||
console.add("! Error while extracting payload.bin")
|
||||
throw IOException()
|
||||
}
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
return when {
|
||||
initBoot.exists() -> {
|
||||
console.add("-- Extract init_boot.img")
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> {
|
||||
console.add("-- Extract boot.img")
|
||||
boot
|
||||
}
|
||||
else -> {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
} catch (e: ErrnoException) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fifo?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
val outFile: MediaStoreUtils.UriFile
|
||||
@@ -470,18 +364,21 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
// raw image
|
||||
outFile = MediaStoreUtils.getFile("$destName.img")
|
||||
outStream = outFile.uri.outputStream()
|
||||
val channel = FileInputStream(uri.openFd("r").fileDescriptor).channel
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
|
||||
try {
|
||||
if (magic.contentEquals("CrAU".toByteArray())) {
|
||||
processPayload(src)
|
||||
DataSourceChannel(channel).use { source ->
|
||||
Payload(source).extract(boot, { console.add(it) }, { logs.add(it) })
|
||||
}
|
||||
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
|
||||
processZip(ZipArchiveInputStream(src))
|
||||
ExtractImage(boot, console, logs).consume(DataSourceChannel(channel))
|
||||
} else {
|
||||
console.add("- Copying image to cache")
|
||||
installDir.getChildFile("boot.img").also {
|
||||
src.copyAndCloseOut(it.newOutputStream())
|
||||
}
|
||||
src.copyAndCloseOut(boot.newOutputStream())
|
||||
}
|
||||
boot
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
@@ -540,7 +437,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
// Download image from url
|
||||
try {
|
||||
srcBoot = installDir.getChildFile("boot.img")
|
||||
ExtractImage(url, console, logs).start(srcBoot)
|
||||
ExtractImage(srcBoot, console, logs)
|
||||
.consume(DataSourceChannel(ServiceLocator.okhttp, url))
|
||||
} catch (e: IOException) {
|
||||
console.add("! Error: " + e.message)
|
||||
Timber.e(e)
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.core.tasks
|
||||
import chromeos_update_engine.UpdateMetadata.DeltaArchiveManifest
|
||||
import chromeos_update_engine.UpdateMetadata.InstallOperation
|
||||
import chromeos_update_engine.UpdateMetadata.PartitionUpdate
|
||||
import com.topjohnwu.magisk.core.utils.HttpFileChannel
|
||||
import com.topjohnwu.magisk.core.utils.DataSourceChannel
|
||||
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream
|
||||
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream
|
||||
import java.io.File
|
||||
@@ -14,7 +14,7 @@ import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.MessageDigest
|
||||
|
||||
class Payload(private val channel: HttpFileChannel) {
|
||||
class Payload(private val channel: DataSourceChannel) {
|
||||
private val manifest: DeltaArchiveManifest
|
||||
private var dataBase = 0L
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
import org.apache.commons.io.input.BoundedInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okio.BufferedSource;
|
||||
|
||||
public class HttpFileChannel implements SeekableByteChannel {
|
||||
public class DataSourceChannel implements SeekableByteChannel {
|
||||
private static final int RANDOM_READ_CACHE_SIZE = 16 * 1024;
|
||||
private static final int SEQ_READ_CACHE_SIZE = 1024 * 1024;
|
||||
private static final int SEQ_READ_THRESHOLD = 1024;
|
||||
@@ -19,6 +22,7 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final String url;
|
||||
private final FileChannel fileChannel;
|
||||
private final long startOffset;
|
||||
private final long size;
|
||||
|
||||
@@ -28,15 +32,21 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
private byte[] cache = null;
|
||||
private long cacheStart = -1;
|
||||
|
||||
public HttpFileChannel(OkHttpClient client, String url, long startOffset, long size) {
|
||||
private DataSourceChannel(OkHttpClient client, String url, FileChannel fileChannel,
|
||||
long startOffset, long size) {
|
||||
this.client = client;
|
||||
this.url = url;
|
||||
this.fileChannel = fileChannel;
|
||||
this.startOffset = startOffset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public HttpFileChannel(OkHttpClient client, String url) throws IOException {
|
||||
this(client, url, 0, fetchTotalSize(client, url));
|
||||
public DataSourceChannel(FileChannel fileChannel) throws IOException {
|
||||
this(null, null, fileChannel, 0, fileChannel.size());
|
||||
}
|
||||
|
||||
public DataSourceChannel(OkHttpClient client, String url) throws IOException {
|
||||
this(client, url, null, 0, fetchTotalSize(client, url));
|
||||
}
|
||||
|
||||
private static long fetchTotalSize(OkHttpClient client, String url) throws IOException {
|
||||
@@ -57,14 +67,14 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
}
|
||||
|
||||
public HttpFileChannel slice(long offset, long sliceSize) {
|
||||
public DataSourceChannel slice(long offset, long sliceSize) {
|
||||
if (offset == 0 && sliceSize == size) {
|
||||
return this;
|
||||
}
|
||||
if (offset < 0 || sliceSize <= 0 || offset + sliceSize >= size) {
|
||||
throw new IllegalArgumentException("Invalid slice parameters");
|
||||
}
|
||||
return new HttpFileChannel(client, url, startOffset + offset, sliceSize);
|
||||
return new DataSourceChannel(client, url, fileChannel, startOffset + offset, sliceSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -170,8 +180,7 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
|
||||
private int readDirectly(ByteBuffer dst, long position) throws IOException {
|
||||
try (var source = streamRead(position, dst.remaining());
|
||||
var channel = Channels.newChannel(source.inputStream())) {
|
||||
try (var channel = Channels.newChannel(streamRead(position, dst.remaining()))) {
|
||||
int totalBytesRead = 0;
|
||||
while (true) {
|
||||
int bytesRead = channel.read(dst);
|
||||
@@ -185,12 +194,23 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
}
|
||||
|
||||
public BufferedSource streamRead(long position, long length) throws IOException {
|
||||
public InputStream streamRead(long position, long length) throws IOException {
|
||||
long endPosition = Math.min(position + length, size) + startOffset;
|
||||
var startPosition = startOffset + position;
|
||||
var readLength = endPosition - startPosition;
|
||||
|
||||
if (fileChannel != null) {
|
||||
fileChannel.position(startPosition);
|
||||
return BoundedInputStream.builder()
|
||||
.setInputStream(Channels.newInputStream(fileChannel))
|
||||
.setMaxCount(readLength)
|
||||
.setPropagateClose(false)
|
||||
.get();
|
||||
}
|
||||
|
||||
var request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Range", "bytes=" + (startOffset + position) + "-" + (endPosition - 1))
|
||||
.header("Range", "bytes=" + startPosition + "-" + (endPosition - 1))
|
||||
.build();
|
||||
|
||||
var response = client.newCall(request).execute();
|
||||
@@ -198,7 +218,7 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
response.close();
|
||||
throw new IOException("Unexpected response code " + response.code());
|
||||
}
|
||||
return response.body().source();
|
||||
return response.body().byteStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -207,7 +227,7 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
public DataSourceChannel position(long newPosition) throws IOException {
|
||||
if (!open) throw new ClosedChannelException();
|
||||
if (newPosition < 0) {
|
||||
throw new IllegalArgumentException("Position out of bounds: " + newPosition);
|
||||
@@ -227,9 +247,12 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public void close() throws IOException {
|
||||
open = false;
|
||||
cache = null;
|
||||
if (fileChannel != null) {
|
||||
fileChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -238,7 +261,7 @@ public class HttpFileChannel implements SeekableByteChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) {
|
||||
public DataSourceChannel truncate(long size) {
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,8 @@ object MediaStoreUtils {
|
||||
|
||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||
|
||||
fun Uri.openFd() = cr.openFileDescriptor(this, "rwt") ?: throw FileNotFoundException()
|
||||
fun Uri.openFd(mode: String = "rwt") = cr.openFileDescriptor(this, mode)
|
||||
?: throw FileNotFoundException()
|
||||
|
||||
val Uri.displayName: String get() {
|
||||
if (scheme == "file") {
|
||||
|
||||
Reference in New Issue
Block a user