chromium/components/crash/android/java/src/org/chromium/components/crash/MinidumpLogcatPrepender.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.crash;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.components.minidump_uploader.CrashFileManager;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

/** Prepends a logcat file to a minidump file for upload. */
public class MinidumpLogcatPrepender {
    private static final String TAG = "LogcatPrepender";

    @VisibleForTesting
    public static final String LOGCAT_CONTENT_DISPOSITION =
            "Content-Disposition: form-data; name=\"logcat\"; filename=\"logcat\"";

    @VisibleForTesting public static final String LOGCAT_CONTENT_TYPE = "Content-Type: text/plain";

    private final CrashFileManager mFileManager;
    private final File mMinidumpFile;
    private final List<String> mLogcat;

    public MinidumpLogcatPrepender(
            CrashFileManager fileManager, File minidumpFile, List<String> logcat) {
        mFileManager = fileManager;
        mMinidumpFile = minidumpFile;
        mLogcat = logcat;
    }

    /** Read the boundary from the first line of the file. */
    private static String getBoundary(File minidumpFile) throws IOException {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(minidumpFile));
            return reader.readLine();
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
    }

    /**
     * Write the logcat data to the specified target {@link File}.
     *
     * Target file is overwritten, not appended to the end.
     *
     * @param targetFile File to which logcat data should be written.
     * @param logcat The lines of the logcat output.
     * @param boundary String MIME boundary to prepend.
     * @throws IOException if something goes wrong.
     */
    private static void writeLogcat(File targetFile, List<String> logcat, String boundary)
            throws IOException {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(targetFile, false));
            writer.write(boundary);
            writer.newLine();
            // Next we write the logcat data in a MIME block.
            writer.write(LOGCAT_CONTENT_DISPOSITION);
            writer.newLine();
            writer.write(LOGCAT_CONTENT_TYPE);
            writer.newLine();
            writer.newLine();
            // Emits the contents of the buffer into the output file.
            for (String ln : logcat) {
                writer.write(ln);
                writer.newLine();
            }
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    /**
     * Append the minidump file data to the specified target {@link File}.
     *
     * @param minidumpFile File containing data to append.
     * @param targetFile File to which data should be appended.
     * @throws IOException when standard IO errors occur.
     */
    private static void appendMinidump(File minidumpFile, File targetFile) throws IOException {
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            byte[] buf = new byte[256];
            in = new BufferedInputStream(new FileInputStream(minidumpFile));
            out = new BufferedOutputStream(new FileOutputStream(targetFile, true));
            int count;
            while ((count = in.read(buf)) != -1) {
                out.write(buf, 0, count);
            }
        } finally {
            if (in != null) in.close();
            if (out != null) out.close();
        }
    }

    /**
     * Prepends the logcat output to the minidump file.
     * @return On success, returns the file containing the combined logcat and minidump output.
     *     On failure, returns the original file containing just the minidump.
     */
    public File run() {
        if (mLogcat.isEmpty()) return mMinidumpFile;

        String targetFileName = mMinidumpFile.getName() + ".try0";
        File targetFile = null;
        boolean success = false;
        try {
            String boundary = getBoundary(mMinidumpFile);
            if (boundary == null) {
                return mMinidumpFile;
            }

            targetFile = mFileManager.createNewTempFile(targetFileName);
            writeLogcat(targetFile, mLogcat, boundary);

            // Finally reopen and append the original minidump MIME sections, including the leading
            // boundary.
            appendMinidump(mMinidumpFile, targetFile);
            success = true;
        } catch (IOException e) {
            Log.w(
                    TAG,
                    "Error while trying to annotate minidump file %s with logcat data",
                    mMinidumpFile.getAbsoluteFile(),
                    e);
            if (targetFile != null) {
                CrashFileManager.deleteFile(targetFile);
            }
        }

        if (!success) return mMinidumpFile;

        // Try to clean up the previous file. Note that this step is best-effort, and failing to
        // perform the cleanup does not count as an overall failure to prepend the logcat output.
        if (!mMinidumpFile.delete()) {
            Log.w(TAG, "Failed to delete minidump file: " + mMinidumpFile.getName());
        }

        assert targetFile != null;
        assert targetFile.exists();
        return targetFile;
    }
}