Loading src/com/android/documentsui/archives/Archive.java +79 −20 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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) { Loading tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java +42 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading
src/com/android/documentsui/archives/Archive.java +79 −20 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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) { Loading
tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java +42 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading