app: add local file supports for HttpFileChannel

This commit is contained in:
vvb2060
2025-09-30 02:54:48 +08:00
committed by John Wu
parent b9d21071fc
commit 4ee1590cc8
5 changed files with 79 additions and 150 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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") {