Home > Software design >  Android 11 Kotlin: Reading a .zip File
Android 11 Kotlin: Reading a .zip File

Time:10-09

I've got an Android app written in Kotlin targeting framework 30 , so I'm working within the new Android 11 file access restrictions. The app needs to be able to open an arbitrary .zip file in the shared storage (chosen interactively by the user) then do stuff with the contents of that .zip file.

I'm getting a URI for the .zip file in what I'm led to understand is the canonical way:

    val activity = this
    val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
        CoroutineScope(Dispatchers.Main).launch {
            if(it != null) doStuffWithZip(activity, it)
            ...
        }
    }
    getContent.launch("application/zip")

My problem is that the Java.util.zip.ZipFile class I'm using only knows how to open a .zip file specified by a String or a File, and I don't have any easy way to get to either of those from a Uri. (I'm guessing that the ZipFile object needs the actual file rather than some kind of stream because it needs to be able to seek...)

The workaround I'm using at present is to turn the Uri into an InputStream, copy the contents to a temp file in private storage, and make a ZipFile instance from that:

        private suspend fun <T> withZipFromUri(
            context: Context,
            uri: Uri, block: suspend (ZipFile) -> T
        ) : T {
            val file = File(context.filesDir, "tempzip.zip")
            try {
                return withContext(Dispatchers.IO) {
                    kotlin.runCatching {
                        context.contentResolver.openInputStream(uri).use { input ->
                            if (input == null) throw FileNotFoundException("openInputStream failed")
                            file.outputStream().use { input.copyTo(it) }
                        }
                        ZipFile(file, ZipFile.OPEN_READ).use { block.invoke(it) }
                    }.getOrThrow()
                }
            } finally {
                file.delete()
            }
        }

Then, I can use it like this:

        suspend fun doStuffWithZip(context: Context, uri: Uri) {
            withZipFromUri(context, uri) { // it: ZipFile
                for (entry in it.entries()) {
                    dbg("entry: ${entry.name}") // or whatever
                }
            }
        }

This works, and (in my particular case, where the .zip file in question is never more than a couple MB) is reasonably performant.

But, I tend to regard programming by temporary file as the last refuge of the terminally incompetent, thus I can't escape the feeling that I'm missing a trick here. (Admittedly, I am terminally incompetent in the context of Android Kotlin, but I'd like to learn to not be...)

Any better ideas? Is there a cleaner way to implement this that doesn't involve making an extra copy of the file?

CodePudding user response:

Copying from external source (and risking downvoting to oblivion) and this isn't quite an answer, but too long for a comment

public class ZipFileUnZipExample {

    public static void main(String[] args) {

        Path source = Paths.get("/home/mkyong/zip/test.zip");
        Path target = Paths.get("/home/mkyong/zip/");

        try {

            unzipFolder(source, target);
            System.out.println("Done");

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void unzipFolder(Path source, Path target) throws IOException {
        // Put the InputStream obtained from Uri here instead of the FileInputStream perhaps?
        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {

            // list files in zip
            ZipEntry zipEntry = zis.getNextEntry();

            while (zipEntry != null) {

                boolean isDirectory = false;
                // example 1.1
                // some zip stored files and folders separately
                // e.g data/
                //     data/folder/
                //     data/folder/file.txt
                if (zipEntry.getName().endsWith(File.separator)) {
                    isDirectory = true;
                }

                Path newPath = zipSlipProtect(zipEntry, target);

                if (isDirectory) {
                    Files.createDirectories(newPath);
                } else {

                    // example 1.2
                    // some zip stored file path only, need create parent directories
                    // e.g data/folder/file.txt
                    if (newPath.getParent() != null) {
                        if (Files.notExists(newPath.getParent())) {
                            Files.createDirectories(newPath.getParent());
                        }
                    }

                    // copy files, nio
                    Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);

                    // copy files, classic
                    /*try (FileOutputStream fos = new FileOutputStream(newPath.toFile())) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, len);
                        }
                    }*/
                }

                zipEntry = zis.getNextEntry();

            }
            zis.closeEntry();

        }

    }

    // protect zip slip attack
    public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
        throws IOException {

        // test zip slip vulnerability
        // Path targetDirResolved = targetDir.resolve("../../"   zipEntry.getName());

        Path targetDirResolved = targetDir.resolve(zipEntry.getName());

        // make sure normalized file still has targetDir as its prefix
        // else throws exception
        Path normalizePath = targetDirResolved.normalize();
        if (!normalizePath.startsWith(targetDir)) {
            throw new IOException("Bad zip entry: "   zipEntry.getName());
        }

        return normalizePath;
    }

}

This apparently works with pre-existing files; however since you already have an InputStream read from the Uri - you can adapt this and give it a shot.

EDIT: It seems like it's extracting to Files as well - you could store the individual ByteArrays somewhere then decide what to do with them later on. But I hope you get the general idea - you can do all of this in-memory, without having to use the disk (temp files or files) in between.

Your requirement is a bit vague and unclear however, so I don't know what you're trying to do, merely suggesting a venue/approach to try out

CodePudding user response:

What about a simple ZipInputStream ? – Shark

Good idea @Shark.

InputSteam is = getContentResolver().openInputStream(uri);

ZipInputStream zis = new ZipInputStream(is);

CodePudding user response:

@Shark has it with ZipInputStream. I'm not sure how I missed that to begin with, but I sure did.

My withZipFromUri() method is much simpler and nicer now:

suspend fun <T> withZipFromUri(
    context: Context,
    uri: Uri, block: suspend (ZipInputStream) -> T
) : T =
    withContext(Dispatchers.IO) {
        kotlin.runCatching {
            context.contentResolver.openInputStream(uri).use { input ->
                if (input == null) throw FileNotFoundException("openInputStream failed")
                ZipInputStream(input).use {
                    block.invoke(it)
                }
            }
        }.getOrThrow()
    }

This isn't call-compatible with the old one (since the block function now takes a ZipInputStream as a parameter rather than a ZipFile). In my particular case -- and really, in any case where the consumer doesn't mind dealing with entries in the order they appear -- that's OK.

CodePudding user response:

Okio (3-Alpha) has a ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt

You could probably combine it with a custom FileSystem that reads the content of that file. It will require a fair bit of code but will be efficient.

This is an example of a custom filesystem https://github.com/square/okio/blob/88fa50645946bc42725d2f33e143628e7892be1b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt

But I suspect it's simpler to convert the URI to a file and avoid any copying or additional code.

  • Related