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

Commit 66d323e6 authored by François Degros's avatar François Degros
Browse files

Improve Archive.getEntryPath()

Archive.getEntryPath() now correctly normalizes paths containing parts
such as "." and "..".

Bug: 384606593
Test: atest DocumentsUIGoogleTests:com.android.documentsui.archives.ArchiveHandleTest#normalizePath
Flag: EXEMPT bug fix
Change-Id: I4dfff0046c1e56c8417a15f70396bc7039c519d3
parent ce255449
Loading
Loading
Loading
Loading
+79 −20
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.documentsui.archives;

import static com.android.documentsui.base.SharedMinimal.DEBUG;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
@@ -29,23 +31,23 @@ import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;

import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;

import org.apache.commons.compress.archivers.ArchiveEntry;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;

/**
 * Provides basic implementation for creating, extracting and accessing
 * files within archives exposed by a document provider.
@@ -90,28 +92,85 @@ public abstract class Archive implements Closeable {
        mEntries = new HashMap<>();
    }

    /**
     * Returns a valid, normalized path for an entry.
     */
    /** Returns a valid, normalized path for an entry. */
    public static String getEntryPath(ArchiveEntry entry) {
        if (entry instanceof ZipArchiveEntry) {
            /**
             * Some of archive entry doesn't have the same naming rule.
             * For example: The name of 7 zip directory entry doesn't end with '/'.
             * Only check for Zip archive.
             */
            Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
                    "Ill-formated ZIP-file.");
        final List<String> parts = new ArrayList<String>();
        boolean isDir = true;

        // Get the path that will be decomposed and normalized
        final String in = entry.getName();

        decompose:
        for (int i = 0; i < in.length(); ) {
            // Skip separators
            if (in.charAt(i) == '/') {
                isDir = true;
                do {
                    if (++i == in.length()) break decompose;
                } while (in.charAt(i) == '/');
            }

            // Found the beginning of a part
            final int b = i;
            assert (b < in.length());
            assert (in.charAt(b) != '/');

            // Find the end of the part
            do {
                ++i;
            } while (i < in.length() && in.charAt(i) != '/');

            // Extract part
            final String part = in.substring(b, i);
            assert (!part.isEmpty());

            // Special case if part is "."
            if (part.equals(".")) {
                isDir = true;
                continue;
            }

            // Special case if part is ".."
            if (part.equals("..")) {
                isDir = true;
                if (!parts.isEmpty()) parts.remove(parts.size() - 1);
                continue;
            }
        if (entry.getName().startsWith("/")) {
            return entry.getName();
        } else {
            return "/" + entry.getName();

            // The part is either a directory or a file name
            isDir = false;
            parts.add(part);
        }

        // If the decomposed path looks like a directory but the archive entry says that it is not
        // a directory entry, append "?" for the file name
        if (isDir && !entry.isDirectory()) {
            isDir = false;
            parts.add("?");
        }

        if (parts.isEmpty()) return "/";

        // Construct the normalized path
        final StringBuilder sb = new StringBuilder(in.length() + 3);

        for (final String part : parts) {
            sb.append('/');
            sb.append(part);
        }

        if (entry.isDirectory()) {
            sb.append('/');
        }

        final String out = sb.toString();
        if (DEBUG) Log.d(TAG, "getEntryPath(" + in + ") -> " + out);
        return out;
    }

    /**
     * Returns true if the file descriptor is seekable.
     *
     * @param descriptor File descriptor to check.
     */
    public static boolean canSeek(ParcelFileDescriptor descriptor) {
+42 −3
Original line number Diff line number Diff line
@@ -142,6 +142,45 @@ public class ArchiveHandleTest {
            new ArchiveEntryRecord("hello/inside_folder/hello_insside.txt", 14, false),
            new ArchiveEntryRecord("hello/hello2.txt", 48, false));

    private static String getNormalizedPath(String in, boolean isDir) {
        return Archive.getEntryPath(new ArchiveEntryRecord(in, -1, isDir));
    }

    @Test
    public void normalizePath() {
        assertThat(getNormalizedPath("", true)).isEqualTo("/");
        assertThat(getNormalizedPath("", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("/", true)).isEqualTo("/");
        assertThat(getNormalizedPath("/", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("///", true)).isEqualTo("/");
        assertThat(getNormalizedPath("///", false)).isEqualTo("/?");
        assertThat(getNormalizedPath(".", true)).isEqualTo("/");
        assertThat(getNormalizedPath(".", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("./", true)).isEqualTo("/");
        assertThat(getNormalizedPath("./", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("./foo", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("./foo", false)).isEqualTo("/foo");
        assertThat(getNormalizedPath("./foo/", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("./foo/", false)).isEqualTo("/foo/?");
        assertThat(getNormalizedPath("..", true)).isEqualTo("/");
        assertThat(getNormalizedPath("..", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("../", true)).isEqualTo("/");
        assertThat(getNormalizedPath("../", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("foo", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("foo", false)).isEqualTo("/foo");
        assertThat(getNormalizedPath("foo/", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("foo/", false)).isEqualTo("/foo/?");
        assertThat(getNormalizedPath("foo/.", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("foo/.", false)).isEqualTo("/foo/?");
        assertThat(getNormalizedPath("foo/..", true)).isEqualTo("/");
        assertThat(getNormalizedPath("foo/..", false)).isEqualTo("/?");
        assertThat(getNormalizedPath("/foo", true)).isEqualTo("/foo/");
        assertThat(getNormalizedPath("/foo", false)).isEqualTo("/foo");
        assertThat(getNormalizedPath("//./../a//b///../c.ext", true)).isEqualTo("/a/c.ext/");
        assertThat(getNormalizedPath("//./../a//b///../c.ext", false)).isEqualTo("/a/c.ext");
        assertThat(getNormalizedPath("//./../a//b///../c.ext/", true)).isEqualTo("/a/c.ext/");
        assertThat(getNormalizedPath("//./../a//b///../c.ext/", false)).isEqualTo("/a/c.ext/?");
    }

    @Test
    public void buildArchiveHandle_withoutFileDescriptor_shouldBeIllegal() throws Exception {