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

package org.chromium.build;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import proguard.retrace.ReTrace;

/**
 * A wrapper around ReTrace that:
 *  1. Hardcodes a more useful line regular expression
 *  2. Disables output buffering
 */
public class FlushingReTrace {
    // E.g.: D/ConnectivityService(18029): Message
    // E.g.: W/GCM     ( 151): Message
    // E.g.: 09-08 14:22:59.995 18029 18055 I ProcessStatsService: Message
    // E.g.: 09-08 14:30:59.145 17731 18020 D MDnsDS  : Message
    private static final String LOGCAT_PREFIX =
            "(?:[VDIWEF]/.*?\\( *\\d+\\): |\\d\\d-\\d\\d [0-9:. ]+[VDIWEF] .*?: )?";

    // Note: Order of these sub-patterns defines their precedence.
    // Note: Deobfuscation of methods without the presense of line numbers basically never works.
    // There is a test for these pattern at //build/android/stacktrace/java_deobfuscate_test.py
    private static final String LINE_PARSE_REGEX =
            // Eagerly match logcat prefix to avoid conflicting with the patterns below.
            LOGCAT_PREFIX
            + "(?:"
            // Based on default ReTrace regex, but with whitespaces allowed in file:line parentheses
            // and "at" changed to to allow :
            // E.g.: 06-22 13:58:02.895  4674  4674 E THREAD_STATE:     bLA.a( PG : 173 )
            // Normal stack trace lines look like:
            // \tat org.chromium.chrome.browser.tab.Tab.handleJavaCrash(Tab.java:682)
            + "(?:.*?(?::|\\bat)\\s+%c\\.%m\\s*\\(\\s*%s(?:\\s*:\\s*%l\\s*)?\\))|"
            // E.g.: Caused by: java.lang.NullPointerException: Attempt to read from field 'int bLA'
            // on a null object reference
            + "(?:.*java\\.lang\\.NullPointerException.*[\"']%t\\s*%c\\.(?:%f|%m\\(%a\\))[\"'].*)|"
            // E.g.: java.lang.VerifyError: bLA
            + "(?:java\\.lang\\.VerifyError: %c)|"
            // E.g.: java.lang.NoSuchFieldError: No instance field e of type L...; in class LbxK;
            + "(?:java\\.lang\\.NoSuchFieldError: No instance field %f of type .*? in class L%C;)|"
            // E.g.: Object of type Clazz was not destroyed... (See LifetimeAssert.java)
            + "(?:.*?Object of type %c .*)|"
            // E.g.: VFY: unable to resolve new-instance 3810 (LSome/Framework/Class;) in Lfoo/Bar;
            + "(?:.*L%C;.*)|"
            // E.g.: END SomeTestClass#someMethod
            + "(?:.*?%c#%m.*?)|"
            // Special-case for a common junit logcat message:
            // E.g.: java.lang.NoClassDefFoundError: SomeFrameworkClass in isTestClass for Foo
            + "(?:.* isTestClass for %c)|"
            // E.g.: Caused by: java.lang.RuntimeException: Intentional Java Crash
            + "(?:Caused by: %c:.*)|"
            // Quoted values and lines that end with a class / class+method:
            // E.g.: The class: Foo
            // E.g.: INSTRUMENTATION_STATUS: class=Foo
            // E.g.: NoClassDefFoundError: SomeFrameworkClass in isTestClass for Foo
            // E.g.: Could not find class 'SomeFrameworkClass', referenced from method Foo.bar
            // E.g.: Could not find method SomeFrameworkMethod, referenced from method Foo.bar
            // E.g.: The member "Foo.bar"
            // E.g.: The class "Foobar"
            // Be careful about matching %c without %m since language tags look like class names.
            + "(?:.*?%c\\.%m)|"
            + "(?:.*?\"%c\\.%m\".*)|"
            + "(?:.*\\b(?:[Cc]lass|[Tt]ype)\\b.*?\"%c\".*)|"
            + "(?:.*\\b(?:[Cc]lass|[Tt]ype)\\b.*?%c)|"
            // E.g.: java.lang.RuntimeException: Intentional Java Crash
            + "(?:%c:.*)|"
            // See if entire line matches a class name (e.g. for manual deobfuscation)
            + "(?:%c)"
            + ")";

    private static void usage() {
        System.err.println("Usage: echo $OBFUSCATED_CLASS | java_deobfuscate Foo.apk.mapping");
        System.err.println("Usage: java_deobfuscate Foo.apk.mapping < foo.log");
        System.err.println("Note: Deobfuscation of symbols outside the context of stack "
                + "traces will work only when lines match the regular expression defined "
                + "in FlushingReTrace.java.");
        System.err.println("Also: Deobfuscation of method names without associated line "
                + "numbers does not seem to work.");
        System.exit(1);
    }

    public static void main(String[] args) {
        if (args.length != 1 || args[0].startsWith("-")) {
            usage();
        }

        File mappingFile = new File(args[0]);
        try {
            LineNumberReader reader = new LineNumberReader(
                    new BufferedReader(new InputStreamReader(System.in, "UTF-8")));

            // Enabling autoFlush is the main difference from ReTrace.main().
            boolean autoFlush = true;
            PrintWriter writer =
                    new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"), autoFlush);

            boolean verbose = false;
            new ReTrace(LINE_PARSE_REGEX, verbose, mappingFile).retrace(reader, writer);
        } catch (IOException ex) {
            // Print a verbose stack trace.
            ex.printStackTrace();
            System.exit(1);
        }

        System.exit(0);
    }
}
