#!/bin/bash
# Copyright (C) 2023 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.

set -e


help() {
    cat <<'EOF'

  dump-jar: Dump java classes in jar files

    Usage:
      dump-jar [-v] CLASS-FILE [...]

        Dump a *.class file

      dump-jar [-v] [-s] [-o OUTPUT-FILENAME] JAR-FILE[: class internal name regex] [...]

        Dump a jar file.

        If a filename contains a ':', then the following part
        will be used to filter files in the jar file that matches against class internal names.

        For example, "file.jar:/MyClass$" will only dump "MyClass" in file.jar.

    Options:
      -v: Enable verbose output.

      -s: Simple output mode, used to check HostStubGen output jars.

      -n: Even simpler output mode; print signature only.

      -o: Write the output to a specified file.
EOF
}

# Parse the options.

verbose=0
simple=0
output=""
while getopts "hvsno:" opt; do
case "$opt" in
    h)
        help
        exit 0
        ;;
    v)
        verbose=1
        ;;
    s)
        simple=1
        ;;
    n)
        simple=2
        ;;
    o)
        output="$OPTARG"
        ;;
    '?')
        help
        exit 1
        ;;
esac
done
shift $(($OPTIND - 1))

JAVAP_OPTS="${JAVAP_OPTS:--v -p -s -sysinfo -constants}"

if (( $simple == 1 )) ; then
  JAVAP_OPTS="-p -c -v"
elif (( $simple == 2 )) ; then
  JAVAP_OPTS="-p"
fi

# Convert the output for `-s` as needed.
filter_output() {
  if (( $simple != 0 )) ; then
    # For "simple output" mode,
    # - Normalize the constant numbers (replace with "#x")
    # - Normalize byte code offsets and other similar numbers. (e.g. "0:" -> "x:")
    # - Remove the constant pool
    # - Remove the line number table
    # - Some other transient lines
    # - Sometimes the javap shows mysterious warnings, so remove them too.
    #
    # Most conversion are simple per-line deletion or simple inline replacement with a regex.
    #
    # But removing the constant pool is a bit tricky. It looks like this in the output:
    #---------------------------
    #Constant pool:
    #    #1 = Methodref          #31.#88       // java/lang/Object."<init>":()V
    #    #2 = Class              #89           // java/lang/UnsupportedOperationException
    #    :
    #{ // Or something, I'm not sure if it always ends with a "{".
    #---------------------------
    # i.e. we want to delete all lines from "Constant pool:" as long as the first character
    # is a space.
    #
    # If we simply use '/^Constant pool:/,/^[^ ]/d', then it'll delete the "Constant pool:"
    # line and "{" line too, but again the last line might be important, so we don't want to
    # delete it.
    #
    # So we instead, use '/^Constant pool:/,/^[^ ]/{/^ /d}', which mean:
    # between lines matching '/^Constant pool:/' and '/^[^ ]/', delete lines that start with
    # a space. (=='/^ /d').
    #
    sed -e 's/#[0-9][0-9]*/#x/g' \
        -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \
        -e '/^Constant pool:/,/^[^ ]/{/^ /d}' \
        -e '/^ *line *[0-9][0-9]*: *[0-9][0-9]*$/d' \
        -e '/^ *SHA-256 checksum/d' \
        -e '/^ *Last modified/d' \
        -e '/^Classfile jar/d' \
        -e '/\[warning\]/d'
  else
    cat # Print as-is.
  fi
}

# Write to the output file (specified with -o) as needed.
write_to_out() {
  if [[ -n "$output" ]] ; then
    cat >"$output"
    echo "Wrote output to $output" 1>&2
  else
    cat # print to stdout
  fi
}

# Read jar file names and remove the .class suffix.
# Also remove non-class files.
to_internal_names() {
    sed -ne 's/\.class$//p'
}

for file in "${@}"; do

    # *.class?
    if echo "$file" | grep -qE '\.class$' ; then
        echo "# Class: $file" 1>&2
        javap $dump_code_opt $JAVAP_OPTS $file

    # *.jar?
    elif echo "$file" | grep -qE '\.jar(:.*)?$' ; then
        # Take the regex. Remove everything up to : in $file
        regex=""
        if [[ "$file" =~ : ]] ; then
            regex="${file##*:}"
        fi

        # Remove everything after ':', inclusively, in $file.
        file="${file%:*}"

        # Print the filename and the regex.
        if (( $simple == 0 )) ; then
          echo -n "# Jar: $file"
          if [[ "$regex" != "" ]] ;then
              echo -n "  (regex: $regex)"
          fi
          echo
        fi

        jar tf "$file" | sort | to_internal_names | grep -- "$regex" | while read -r class ; do
            echo "## Class: $class.class"
            javap $dump_code_opt $JAVAP_OPTS -cp "$file" "${class}"
        done

    else
        echo "Unknown file type: $file" 1>&2
        exit 1
    fi
done | filter_output | write_to_out
