<commit_before>/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.provider;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsProvider;
import android.support.annotation.Nullable;
import android.util.Log;
import android.webkit.MimeTypeMap;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.lang.IllegalArgumentException;
import java.lang.IllegalStateException;
import java.lang.UnsupportedOperationException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/**
 * Provides basic implementation for creating, extracting and accessing
 * files within archives exposed by a document provider. The id delimiter
 * must be a character which is not used in document ids generated by the
 * document provider.
 *
 * <p>This class is thread safe.
 *
 * @hide
 */
public class DocumentArchive implements Closeable {
    private static final String TAG = "DocumentArchive";

    private static final String[] DEFAULT_PROJECTION = new String[] {
            Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_DISPLAY_NAME,
            Document.COLUMN_MIME_TYPE,
            Document.COLUMN_SIZE,
            Document.COLUMN_FLAGS
    };

    private final Context mContext;
    private final String mDocumentId;
    private final char mIdDelimiter;
    private final Uri mNotificationUri;
    private final ZipFile mZipFile;
    private final ExecutorService mExecutor;
    private final Map<String, List<ZipEntry>> mTree;

    private DocumentArchive(
            Context context,
            File file,
            String documentId,
            char idDelimiter,
            @Nullable Uri notificationUri)
            throws IOException {
        mContext = context;
        mDocumentId = documentId;
        mIdDelimiter = idDelimiter;
        mNotificationUri = notificationUri;
        mZipFile = new ZipFile(file);
        mExecutor = Executors.newSingleThreadExecutor();

        // Build the tree structure in memory.
        mTree = new HashMap<String, List<ZipEntry>>();
        mTree.put("/", new ArrayList<ZipEntry>());

        // Traversing entries via ZipFile.entries() is very slow, so copy the entries to a temporary
        // map for building the tree.
        final Map<String, ZipEntry> entries = new HashMap<String, ZipEntry>();
        for (final ZipEntry entry : Collections.list(mZipFile.entries())) {
            entries.put(entry.getName(), entry);
            if (entry.isDirectory()) {
                mTree.put(entry.getName(), new ArrayList<ZipEntry>());
            }
        }

        int delimiterIndex;
        List<ZipEntry> parentList;
        String parentPath;
        for (final ZipEntry entry : entries.values()) {
            delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
                    ? entry.getName().length() - 2 : entry.getName().length() - 1);
            parentPath =
                    delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
            parentList = mTree.get(parentPath);
            if (parentList != null) {
                parentList.add(entry);
            } else {
                Log.w(TAG, "Archived files without a parent are not supported: " + entry.getName());
            }
        }
    }

    /**
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * documents within the archive passed as a local file.
     *
     * @param context Context of the provider.
     * @param File Local file containing the archive.
     * @param documentId ID of the archive document.
     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
     *            The delimiter must never be used for IDs of other documents.
     * @param Uri notificationUri Uri for notifying that the archive file has changed.
     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
     *          Uri)
     */
    public static DocumentArchive createForLocalFile(
            Context context, File file, String documentId, char idDelimiter,
            @Nullable Uri notificationUri)
            throws IOException {
        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
    }

    /**
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * documents within the archive passed as a file descriptor.
     *
     * <p>Note, that this method should be used only if the document does not exist
     * on the local storage. A snapshot file will be created, which may be slower
     * and consume significant resources, in contrast to using
     * {@see createForLocalFile(Context, File, String, char, Uri}.
     *
     * @param context Context of the provider.
     * @param descriptor File descriptor for the archive's contents.
     * @param documentId ID of the archive document.
     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
     *            The delimiter must never be used for IDs of other documents.
     * @param Uri notificationUri Uri for notifying that the archive file has changed.
     * @see createForLocalFile(Context, File, String, char, Uri)
     */
    public static DocumentArchive createForParcelFileDescriptor(
            Context context, ParcelFileDescriptor descriptor, String documentId,
            char idDelimiter, @Nullable Uri notificationUri)
            throws IOException {
        File snapshotFile = null;
        try {
            // Create a copy of the archive, as ZipFile doesn't operate on streams.
            // Moreover, ZipInputStream would be inefficient for large files on
            // pipes.
            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
                    "}.zip", context.getCacheDir());

            try (
                final FileOutputStream outputStream =
                        new ParcelFileDescriptor.AutoCloseOutputStream(
                                ParcelFileDescriptor.open(
                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
            ) {
                final byte[] buffer = new byte[32 * 1024];
                int bytes;
                while ((bytes = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytes);
                }
                outputStream.flush();
                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
                        notificationUri);
            }
        } finally {
            // On UNIX the file will be still available for processes which opened it, even
            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
            if (snapshotFile != null) {
                snapshotFile.delete();
            }
        }
    }

    /**
     * Lists child documents of an archive or a directory within an
     * archive. Must be called only for archives with supported mime type,
     * or for documents within archives.
     *
     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
     */
    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
            @Nullable String sortOrder) {
        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
                documentId, mIdDelimiter);
        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
                "Mismatching document ID. Expected: %s, actual: %s.");

        final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
        final MatrixCursor result = new MatrixCursor(
                projection != null ? projection : DEFAULT_PROJECTION);
        if (mNotificationUri != null) {
            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
        }

        final List<ZipEntry> parentList = mTree.get(parentPath);
        Preconditions.checkArgumentNotNull(
                parentList, "The requested directory does not exist in the archive.");
        for (final ZipEntry entry : parentList) {
            addCursorRow(result, entry);
        }
        return result;
    }

    /**
     * Returns a MIME type of a document within an archive.<commit_msg>Throw FileNotFoundException in queryChildDocuments for archives.<commit_after>/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.provider;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsProvider;
import android.support.annotation.Nullable;
import android.util.Log;
import android.webkit.MimeTypeMap;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.lang.IllegalArgumentException;
import java.lang.IllegalStateException;
import java.lang.UnsupportedOperationException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/**
 * Provides basic implementation for creating, extracting and accessing
 * files within archives exposed by a document provider. The id delimiter
 * must be a character which is not used in document ids generated by the
 * document provider.
 *
 * <p>This class is thread safe.
 *
 * @hide
 */
public class DocumentArchive implements Closeable {
    private static final String TAG = "DocumentArchive";

    private static final String[] DEFAULT_PROJECTION = new String[] {
            Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_DISPLAY_NAME,
            Document.COLUMN_MIME_TYPE,
            Document.COLUMN_SIZE,
            Document.COLUMN_FLAGS
    };

    private final Context mContext;
    private final String mDocumentId;
    private final char mIdDelimiter;
    private final Uri mNotificationUri;
    private final ZipFile mZipFile;
    private final ExecutorService mExecutor;
    private final Map<String, List<ZipEntry>> mTree;

    private DocumentArchive(
            Context context,
            File file,
            String documentId,
            char idDelimiter,
            @Nullable Uri notificationUri)
            throws IOException {
        mContext = context;
        mDocumentId = documentId;
        mIdDelimiter = idDelimiter;
        mNotificationUri = notificationUri;
        mZipFile = new ZipFile(file);
        mExecutor = Executors.newSingleThreadExecutor();

        // Build the tree structure in memory.
        mTree = new HashMap<String, List<ZipEntry>>();
        mTree.put("/", new ArrayList<ZipEntry>());

        // Traversing entries via ZipFile.entries() is very slow, so copy the entries to a temporary
        // map for building the tree.
        final Map<String, ZipEntry> entries = new HashMap<String, ZipEntry>();
        for (final ZipEntry entry : Collections.list(mZipFile.entries())) {
            entries.put(entry.getName(), entry);
            if (entry.isDirectory()) {
                mTree.put(entry.getName(), new ArrayList<ZipEntry>());
            }
        }

        int delimiterIndex;
        List<ZipEntry> parentList;
        String parentPath;
        for (final ZipEntry entry : entries.values()) {
            delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
                    ? entry.getName().length() - 2 : entry.getName().length() - 1);
            parentPath =
                    delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
            parentList = mTree.get(parentPath);
            if (parentList != null) {
                parentList.add(entry);
            } else {
                Log.w(TAG, "Archived files without a parent are not supported: " + entry.getName());
            }
        }
    }

    /**
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * documents within the archive passed as a local file.
     *
     * @param context Context of the provider.
     * @param File Local file containing the archive.
     * @param documentId ID of the archive document.
     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
     *            The delimiter must never be used for IDs of other documents.
     * @param Uri notificationUri Uri for notifying that the archive file has changed.
     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
     *          Uri)
     */
    public static DocumentArchive createForLocalFile(
            Context context, File file, String documentId, char idDelimiter,
            @Nullable Uri notificationUri)
            throws IOException {
        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
    }

    /**
     * Creates a DocumentsArchive instance for opening, browsing and accessing
     * documents within the archive passed as a file descriptor.
     *
     * <p>Note, that this method should be used only if the document does not exist
     * on the local storage. A snapshot file will be created, which may be slower
     * and consume significant resources, in contrast to using
     * {@see createForLocalFile(Context, File, String, char, Uri}.
     *
     * @param context Context of the provider.
     * @param descriptor File descriptor for the archive's contents.
     * @param documentId ID of the archive document.
     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
     *            The delimiter must never be used for IDs of other documents.
     * @param Uri notificationUri Uri for notifying that the archive file has changed.
     * @see createForLocalFile(Context, File, String, char, Uri)
     */
    public static DocumentArchive createForParcelFileDescriptor(
            Context context, ParcelFileDescriptor descriptor, String documentId,
            char idDelimiter, @Nullable Uri notificationUri)
            throws IOException {
        File snapshotFile = null;
        try {
            // Create a copy of the archive, as ZipFile doesn't operate on streams.
            // Moreover, ZipInputStream would be inefficient for large files on
            // pipes.
            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
                    "}.zip", context.getCacheDir());

            try (
                final FileOutputStream outputStream =
                        new ParcelFileDescriptor.AutoCloseOutputStream(
                                ParcelFileDescriptor.open(
                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
            ) {
                final byte[] buffer = new byte[32 * 1024];
                int bytes;
                while ((bytes = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytes);
                }
                outputStream.flush();
                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
                        notificationUri);
            }
        } finally {
            // On UNIX the file will be still available for processes which opened it, even
            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
            if (snapshotFile != null) {
                snapshotFile.delete();
            }
        }
    }

    /**
     * Lists child documents of an archive or a directory within an
     * archive. Must be called only for archives with supported mime type,
     * or for documents within archives.
     *
     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
     */
    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
            @Nullable String sortOrder) throws FileNotFoundException {
        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
                documentId, mIdDelimiter);
        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
                "Mismatching document ID. Expected: %s, actual: %s.");

        final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
        final MatrixCursor result = new MatrixCursor(
                projection != null ? projection : DEFAULT_PROJECTION);
        if (mNotificationUri != null) {
            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
        }

        final List<ZipEntry> parentList = mTree.get(parentPath);
        if (parentList == null) {
            throw new FileNotFoundException();
        }
        for (final ZipEntry entry : parentList) {
            addCursorRow(result, entry);
        }
        return result;
    }

    /**
     * Returns a MIME type of a document within an archive.
{'fraction_non_alphanumeric': 0.07736040609137056, 'fraction_numerical': 0.0020304568527918783, 'mean_word_length': 2.5554953979426096, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3750998', 'n_tokens_mistral': 4926, 'n_tokens_neox': 4718, 'n_words': 1639}