Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d5fb54b authored by François Degros's avatar François Degros
Browse files

Check CRC and size when extracting file from archive

Add tests for bad CRCs and bad file sizes.

Bug: 410470676
Bug: 410997889
Bug: 410997606
Bug: 420506655
Bug: 420505627
Flag: com.android.documentsui.flags.use_material3
Flag: com.android.documentsui.flags.zip_ng_ro
Test: atest DocumentsUIGoogleTests:com.android.documentsui.archives
Test: atest DocumentsUIGoogleTests:com.android.documentsui.services.UnpackJobTest

Change-Id: I09244d925911bcacb36fd8d47837301ec3a75983
parent 916d5be4
Loading
Loading
Loading
Loading
+65 −9
Original line number Diff line number Diff line
@@ -16,12 +16,16 @@

package com.android.documentsui.archives;

import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;

import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
@@ -29,17 +33,34 @@ import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.CRC32;
import java.util.zip.Checksum;

/**
 * To simulate the input stream by using ZipFile, SevenZFile, or ArchiveInputStream.
 */
abstract class ArchiveEntryInputStream extends InputStream {
    private final long mSize;
    private final ReadSource mReadSource;

    private ArchiveEntryInputStream(ReadSource readSource, @NonNull ArchiveEntry archiveEntry) {
    private final @NonNull ReadSource mReadSource;
    /** Expected number of bytes if the data being extracted. */
    private final long mExpectedSize;
    /** Number of bytes having been extracted so far. */
    private long mAccumulatedSize = 0;
    /** Expected CRC when all the data has been extracted, or -1 if no CRC needs to be checked. */
    private long mExpectedCrc = -1;
    /** CRC accumulator, or null if no CRC needs to be checked. */
    private @Nullable Checksum mCrcComputer = null;

    private ArchiveEntryInputStream(@NonNull ReadSource readSource, @NonNull ArchiveEntry entry) {
        mReadSource = readSource;
        mSize = archiveEntry.getSize();
        mExpectedSize = entry.getSize();
        if (isZipNgFlagEnabled()) {
            if (entry instanceof ZipArchiveEntry) {
                mExpectedCrc = ((ZipArchiveEntry) entry).getCrc();
            } else if (entry instanceof SevenZArchiveEntry) {
                mExpectedCrc = ((SevenZArchiveEntry) entry).getCrcValue();
            }
            if (mExpectedCrc >= 0) mCrcComputer = new CRC32();
        }
    }

    @Override
@@ -49,16 +70,51 @@ abstract class ArchiveEntryInputStream extends InputStream {

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (mReadSource != null) {
            return mReadSource.read(b, off, len);
        if (mReadSource == null) return -1;

        final int n = mReadSource.read(b, off, len);

        if (n >= 0) {
            if (isZipNgFlagEnabled()) {
                mAccumulatedSize += n;
                if (mAccumulatedSize > mExpectedSize) {
                    throw new IOException(
                            "Extracted file is too long: Already extracted " + mAccumulatedSize
                                    + " bytes when only " + mExpectedSize + " bytes are expected");
                }

                if (mCrcComputer != null) mCrcComputer.update(b, off, n);
            }

            return n;
        }

        // End of stream.
        if (isZipNgFlagEnabled()) {
            // Check file size.
            if (mAccumulatedSize != mExpectedSize) {
                throw new IOException(
                        "Extracted file is too short: Only extracted " + mAccumulatedSize
                                + " bytes when " + mExpectedSize + " bytes are expected");
            }

            // Check CRC.
            if (mCrcComputer != null) {
                final long crc = mCrcComputer.getValue();
                mCrcComputer = null;
                if (crc != mExpectedCrc) {
                    throw new IOException(
                            String.format("Bad CRC: got %08X, want %08X", crc, mExpectedCrc));
                }
            }
        }

        return -1; /* end of input stream */
        return -1;
    }

    @Override
    public int available() throws IOException {
        return mReadSource == null ? 0 : StrictMath.toIntExact(mSize);
        return mReadSource == null ? 0 : StrictMath.toIntExact(mExpectedSize);
    }

    /**
+234 B

File added.

No diff preview for this file type.

+886 B

File added.

No diff preview for this file type.

+199 −0
Original line number Diff line number Diff line
@@ -21,20 +21,29 @@ import android.app.Notification.EXTRA_PROGRESS_INDETERMINATE
import android.app.Notification.EXTRA_TEXT
import android.app.Notification.EXTRA_TITLE
import android.net.Uri
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.provider.DocumentsContract.buildDocumentUri
import android.util.Log
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.documentsui.base.DocumentInfo
import com.android.documentsui.flags.Flags.FLAG_USE_MATERIAL3
import com.android.documentsui.flags.Flags.FLAG_ZIP_NG_RO
import com.android.documentsui.rules.CheckAndForceMaterial3Flag
import com.android.documentsui.services.FileOperationService.OPERATION_UNPACK
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.io.File
import org.junit.Rule
import org.junit.Test

/** Tests UnpackJob. */
@MediumTest
internal class UnpackJobTest : AbstractJobTest<UnpackJob>() {
    @get:Rule
    val checkFlags = CheckAndForceMaterial3Flag()

    /** Tests with a MIME type that is not a supported archive type. */
    @Test
    fun unsupportedMimeType() {
@@ -436,6 +445,196 @@ internal class UnpackJobTest : AbstractJobTest<UnpackJob>() {
        )
    }

    /** Tests with a ZIP archive containing a corrupted file that can be detected via its CRC. */
    @Test
    @RequiresFlagsEnabled(FLAG_USE_MATERIAL3, FLAG_ZIP_NG_RO)
    fun badCrcChecked() {
        val uri = createDocument("application/zip", "archives/zip/bad-crc.zip")
        assertTreeIs(mutableMapOf("/bad-crc.zip" to 234))

        val job = createJob(uri)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_CREATED)
            assertThat(hasFailures).isFalse()
            assertThat(currentBytes).isEqualTo(0)
            assertThat(requiredBytes).isEqualTo(0)
            assertThat(msRemaining).isLessThan(0)
        }

        job.run()

        // The file with a bad CRC should be detected.
        mJobListener.assertFailed()
        mJobListener.assertFailureCount(1)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_COMPLETED)
            assertThat(hasFailures).isTrue()
            assertThat(msg).isEqualTo("Extracting “bad-crc.zip” to “TEST_ROOT_0”")
            assertThat(currentBytes).isEqualTo(0)
            assertThat(requiredBytes).isEqualTo(0)
            assertThat(msRemaining).isLessThan(0)
            assertThat(destination!!.peek().displayName).isEqualTo("bad-crc")
        }

        // The partially extracted file with a bad CRC should have been removed.
        assertTreeIs(
            mutableMapOf(
                "/bad-crc.zip" to 234,
                "/bad-crc/" to -1,
            )
        )
    }

    @Test
    @RequiresFlagsDisabled(FLAG_ZIP_NG_RO)
    fun badCrcUnchecked() {
        val uri = createDocument("application/zip", "archives/zip/bad-crc.zip")
        assertTreeIs(mutableMapOf("/bad-crc.zip" to 234))

        val job = createJob(uri)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_CREATED)
            assertThat(hasFailures).isFalse()
            assertThat(currentBytes).isEqualTo(0)
            assertThat(requiredBytes).isEqualTo(0)
            assertThat(msRemaining).isLessThan(0)
        }

        job.run()

        // The file with a bad CRC should not be detected.
        mJobListener.assertFinished()

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_COMPLETED)
            assertThat(hasFailures).isFalse()
            assertThat(msg).isEqualTo("Extracting “bad-crc.zip” to “TEST_ROOT_0”")
            assertThat(currentBytes).isEqualTo(62)
            assertThat(requiredBytes).isEqualTo(62)
            assertThat(msRemaining).isLessThan(0)
            assertThat(destination!!.peek().displayName).isEqualTo("bad-crc")
        }

        assertTreeIs(
            mutableMapOf(
                "/bad-crc.zip" to 234,
                "/bad-crc/" to -1,
                "/bad-crc/bad-crc.txt" to 62,
            )
        )
    }

    /** Tests with a ZIP archive containing a corrupted repository with wrong file sizes. */
    @Test
    @RequiresFlagsEnabled(FLAG_USE_MATERIAL3, FLAG_ZIP_NG_RO)
    fun badSizesChecked() {
        val uri = createDocument("application/zip", "archives/zip/bad-sizes.zip")
        assertTreeIs(mutableMapOf("/bad-sizes.zip" to 886))

        val job = createJob(uri)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_CREATED)
            assertThat(hasFailures).isFalse()
            assertThat(currentBytes).isEqualTo(0)
            assertThat(requiredBytes).isEqualTo(0)
            assertThat(msRemaining).isLessThan(0)
        }

        job.run()

        // The files with incorrect sizes should be detected.
        mJobListener.assertFailed()
        mJobListener.assertFailureCount(7)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_COMPLETED)
            assertThat(hasFailures).isTrue()
            assertThat(msg).isEqualTo("Extracting “bad-sizes.zip” to “TEST_ROOT_0”")
            assertThat(currentBytes).isEqualTo(3)
            assertThat(requiredBytes).isEqualTo(3)
            assertThat(msRemaining).isLessThan(0)
            assertThat(destination!!.peek().displayName).isEqualTo("bad-sizes")
        }

        // Only the file with the correct size should have been extracted.
        assertTreeIs(
            mutableMapOf(
                "/bad-sizes.zip" to 886,
                "/bad-sizes/" to -1,
                "/bad-sizes/d/" to -1,
                "/bad-sizes/3.txt" to 3,
            )
        )
    }

    @Test
    @RequiresFlagsDisabled(FLAG_ZIP_NG_RO)
    fun badSizesUnchecked() {
        val uri = createDocument("application/zip", "archives/zip/bad-sizes.zip")
        assertTreeIs(mutableMapOf("/bad-sizes.zip" to 886))

        val job = createJob(uri)

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_CREATED)
            assertThat(hasFailures).isFalse()
            assertThat(currentBytes).isEqualTo(0)
            assertThat(requiredBytes).isEqualTo(0)
            assertThat(msRemaining).isLessThan(0)
        }

        job.run()

        // The files with incorrect sizes should not be detected.
        mJobListener.assertFinished()

        with(job.getJobProgress()) {
            assertThat(operationType).isEqualTo(OPERATION_UNPACK)
            assertThat(id).isEqualTo(job.id)
            assertThat(state).isEqualTo(Job.STATE_COMPLETED)
            assertThat(hasFailures).isFalse()
            assertThat(msg).isEqualTo("Extracting “bad-sizes.zip” to “TEST_ROOT_0”")
            assertThat(currentBytes).isEqualTo(28)
            assertThat(requiredBytes).isEqualTo(28)
            assertThat(msRemaining).isLessThan(0)
            assertThat(destination!!.peek().displayName).isEqualTo("bad-sizes")
        }

        assertTreeIs(
            mutableMapOf(
                "/bad-sizes.zip" to 886,
                "/bad-sizes/" to -1,
                "/bad-sizes/d/" to -1,
                "/bad-sizes/0.txt" to 0,
                "/bad-sizes/1.txt" to 1,
                "/bad-sizes/2.txt" to 2,
                "/bad-sizes/3.txt" to 3,
                "/bad-sizes/4.txt" to 4,
                "/bad-sizes/5.txt" to 5,
                "/bad-sizes/6.txt" to 6,
                "/bad-sizes/7.txt" to 7,
            )
        )
    }

    /** Tests with an empty ZIP archive. */
    @Test
    fun emptyZip() {