#!/bin/bash -eu
#
# Copyright 2015 The Bazel Authors. All rights reserved.
# 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

# TODO(bazel-team) test that modifying the source in a non-interface
#   changing way results in the same -interface.jar.

DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

## Inputs
JAVAC=$1
shift
JAVA=$1
shift
JAR=$1
shift
JAVAP=$1
shift
IJAR=$1
shift
LANGTOOLS8=$1
shift
UNZIP=$1
shift
ZIP=$1
shift
ZIP_COUNT=$1
shift

## Test framework
source ${DIR}/testenv.sh || { echo "testenv.sh not found!" >&2; exit 1; }

function cleanup() {
  rm -fr "$TEST_TMPDIR"/*
}

trap cleanup EXIT

## Tools
# Ensure that tooling path is absolute if not in PATH.
[[ "$JAVAC" =~ ^(/|[^/]+$) ]] || JAVAC="$PWD/$JAVAC"
[[ "$JAR" =~ ^(/|[^/]+$) ]] || JAR="$PWD/$JAR"
[[ "$IJAR" =~ ^(/|[^/]+$) ]] || IJAR="$PWD/$IJAR"
[[ "$UNZIP" =~ ^(/|[^/]+$) ]] || UNZIP="$PWD/$UNZIP"
[[ "$ZIP" =~ ^(/|[^/]+$) ]] || ZIP="$PWD/$ZIP"
[[ "$JAVAP" =~ ^(/|[^/]+$) ]] || JAVAP="$PWD/$JAVAP"
[[ "$ZIP_COUNT" =~ ^(/|[^/]+$) ]] || ZIP_COUNT="$PWD/$ZIP_COUNT"

IJAR_SRCDIR=$(dirname ${IJAR})
A_JAR=$TEST_TMPDIR/A.jar
A_INTERFACE_JAR=$TEST_TMPDIR/A-interface.jar
A_ZIP_JAR=$TEST_TMPDIR/A_zip.jar
A_ZIP_INTERFACE_JAR=$TEST_TMPDIR/A_zip-interface.jar
W_JAR=$TEST_TMPDIR/W.jar
BOTTLES_JAR=$TEST_TMPDIR/bottles.jar
JAR_WRONG_CENTRAL_DIR=$IJAR_SRCDIR/test/libwrongcentraldir.jar
IJAR_WRONG_CENTRAL_DIR=$TEST_TMPDIR/wrongcentraldir_interface.jar
OBJECT_JAVA=$IJAR_SRCDIR/test/Object.java
OBJECT_JAR=$TEST_TMPDIR/object.jar
OBJECT_IJAR=$TEST_TMPDIR/object_interface.jar
TYPEANN2_JAR=$IJAR_SRCDIR/test/libtypeannotations2.jar
TYPEANN2_IJAR=$TEST_TMPDIR/typeannotations2_interface.jar
TYPEANN2_JAVA=$IJAR_SRCDIR/test/TypeAnnotationTest2.java
INVOKEDYNAMIC_JAR=$IJAR_SRCDIR/test/libinvokedynamic.jar
INVOKEDYNAMIC_IJAR=$TEST_TMPDIR/invokedynamic_interface.jar
METHODPARAM_JAR=$IJAR_SRCDIR/test/libmethodparameters.jar
METHODPARAM_IJAR=$TEST_TMPDIR/methodparameters_interface.jar
SOURCEDEBUGEXT_JAR=$IJAR_SRCDIR/test/source_debug_extension.jar
SOURCEDEBUGEXT_IJAR=$TEST_TMPDIR/source_debug_extension.jar
CENTRAL_DIR_LARGEST_REGULAR=$IJAR_SRCDIR/test/largest_regular.jar
CENTRAL_DIR_SMALLEST_ZIP64=$IJAR_SRCDIR/test/smallest_zip64.jar
CENTRAL_DIR_ZIP64=$IJAR_SRCDIR/test/definitely_zip64.jar

#### Setup

# set_file_length FILE SIZE
#
# Sets the file size for FILE, truncating if necessary, creating a
# sparse file if possible, preserving original contents if they fit.
function set_file_length() {
  perl -e 'open(FH, ">>$ARGV[0]") && truncate(FH, $ARGV[1]) or fail $!' "$@" &&
  [[ "$(statfmt $1)" == "$2" ]] ||
  fail "set_file_length failed"
}

# check_consistent_file_contents FILE
#
# Checks that all files created with the given filename have identical contents.
expected_output=""
function check_consistent_file_contents() {
  local actual="$(cat $1 | ${MD5SUM} | awk '{ print $1; }')"
  local filename="$(echo $1 | ${MD5SUM} | awk '{ print $1; }')"
  local expected="$actual"
  if (echo "${expected_output}" | grep -q "^${filename} "); then
    echo "${expected_output}" | grep -q "^${filename} ${actual}$" || {
      ls -l "$1"
      fail "output file contents differ"
    }
  else
    expected_output="$expected_output$filename $actual
"
  fi
}

function set_up() {
  mkdir -p $TEST_TMPDIR/classes
}

function tear_down() {
  rm -fr $TEST_TMPDIR/classes
}

#### Tests
function test_output_bigger_than_input() {
  # Tests that ijar does not crash when output ijar is bigger than the input jar
  $JAVAC -g -d $TEST_TMPDIR/classes \
    $IJAR_SRCDIR/test/WellCompressed*.java ||
    fail "javac failed"
  $JAR cf $W_JAR -C $TEST_TMPDIR/classes . || fail "jar failed"

  W_INTERFACE_JAR=$TEST_TMPDIR/W-interface.jar
  $IJAR $W_JAR $W_INTERFACE_JAR || fail "ijar failed"
  # Check that the interface jar is bigger than the original jar.
  W_JAR_SIZE=$(statfmt $W_JAR)
  W_INTERFACE_JAR_SIZE=$(statfmt $W_INTERFACE_JAR)
  [[ $W_INTERFACE_JAR_SIZE -gt $W_JAR_SIZE ]] || fail "interface jar should be bigger"
}

function test_class_more_64k() {
  # Tests that ijar can handle class bodies longer than 64K
  # First, generate the input file
  BOTTLES_JAVA=$TEST_TMPDIR/BottlesOnTheWall.java
  echo "public class BottlesOnTheWall {" > $BOTTLES_JAVA
  for i in $(seq 1 16384); do
    echo "  public int getBottleOnTheWall${i}() { return ${i}; }" >> $BOTTLES_JAVA
  done

  echo "}" >> $BOTTLES_JAVA

  $JAVAC -g -d $TEST_TMPDIR/classes $BOTTLES_JAVA || fail "javac failed"
  BOTTLES_INTERFACE_JAR=$TEST_TMPDIR/bottles-interface.jar

  # Test ijar calls
  for flag0 in '' '0'; do
    $JAR c"${flag0}"f $BOTTLES_JAR -C $TEST_TMPDIR/classes . || fail "jar failed"
    $IJAR $BOTTLES_JAR $BOTTLES_INTERFACE_JAR || fail "ijar failed"
    check_consistent_file_contents $BOTTLES_INTERFACE_JAR
  done

  # Check that ijar can produce class files with a body longer than 64K by
  # calling ijar itself on the output file to make sure that it is valid
  BOTTLES_INTERFACE_INTERFACE_JAR=$TEST_TMPDIR/bottles-interface-interface.jar
  $IJAR $BOTTLES_INTERFACE_JAR $BOTTLES_INTERFACE_INTERFACE_JAR ||
    fail "ijar cannot produce class files with body longer than 64K"
}

function test_ijar_output() {
  # Numerous check on the output created by ijar.

  # Compiles A.java, builds A.jar and A-interface.jar
  $JAVAC -g -d $TEST_TMPDIR/classes $IJAR_SRCDIR/test/A.java ||
    fail "javac failed"
  $JAR cf $A_JAR -C $TEST_TMPDIR/classes . || fail "jar failed"
  $IJAR $A_JAR $A_INTERFACE_JAR || fail "ijar failed."

  # Check that the number of entries is 5:
  #  A, A.PrivateInner, A.PublicInner, A.MyAnnotation,
  #  A.RuntimeAnnotation
  # (Note: even private inner classes are retained, so we don't need to change
  # the types of members.)
  expected=5
  lines=$($JAR tvf $A_INTERFACE_JAR | wc -l)
  check_eq $expected $lines "Interface jar should have $expected entries!"

  # Check that no private class members are found:
  lines=$($JAVAP -private -classpath $A_JAR A | grep priv | wc -l)
  check_eq 2 $lines "Input jar should have 2 private members!"
  lines=$($JAVAP -private -classpath $A_INTERFACE_JAR A | grep priv | wc -l)
  check_eq 0 $lines "Interface jar should have no private members!"
  lines=$($JAVAP -private -classpath $A_INTERFACE_JAR A | grep clinit | wc -l)
  check_eq 0 $lines "Interface jar should have no class initializers!"


  # Check that no code is found:
  lines=$($JAVAP -c -private -classpath $A_JAR A | grep Code: | wc -l)
  check_eq 5 $lines "Input jar should have 5 method bodies!"
  lines=$($JAVAP -c -private -classpath $A_INTERFACE_JAR A | grep Code: | wc -l)
  check_eq 0 $lines "Interface jar should have no method bodies!"

  # Check that constants from code are no longer present:
  $JAVAP -c -private -classpath $A_JAR A | grep -sq foofoofoofoo ||
    fail "Input jar should have code constants!"
  $JAVAP -c -private -classpath $A_INTERFACE_JAR A | grep -sq foofoofoofoo &&
    fail "Interface jar should have no code constants!"


  # Check (important, this!) that the interface jar is still sufficient
  # for compiling:
  $JAVAC -Xlint -classpath $A_INTERFACE_JAR -g -d $TEST_TMPDIR/classes \
    $IJAR_SRCDIR/test/B.java 2>$TEST_log || fail "Can't compile B!"

  # Test compilation of B yielded deprecation message:
  expect_log 'deprecatedMethod.*in A has been deprecated' \
    "ijar has lost @Deprecated annotation!"

  # Run the dynamic checks in B.main().
  $JAVA -classpath $TEST_TMPDIR/classes B || exit 1

  # Check idempotence of ijar transformation:
  A_INTERFACE_INTERFACE_JAR=$TEST_TMPDIR/A-interface-interface.jar
  $IJAR $A_INTERFACE_JAR $A_INTERFACE_INTERFACE_JAR || fail "ijar failed."
  cmp $A_INTERFACE_JAR $A_INTERFACE_INTERFACE_JAR ||
    fail "ijar transformation is not idempotent"


  # Check that -interface.jar contains nothing but .class files:
  check_eq 0 $($JAR tf $A_INTERFACE_JAR | grep -v \\.class$ | wc -l) \
    "Interface jar should contain only .class files!"


  # Check that -interface.jar timestamps are normalized:
  check_eq 0 $(TZ=UTC $JAR tvf $A_INTERFACE_JAR |
               grep -v 'Tue Jan 01 00:00:00 UTC 1980' | wc -l) \
   "Interface jar contained non-zero timestamps!"


  # Check that compile-time constants in A are still annotated as such in ijar:
  $JAVAP -classpath $TEST_TMPDIR/classes -c B | grep -sq ldc2_w.*123 ||
    fail "ConstantValue not propagated to class B!"


  # Check that a jar compressed with zip results in the same interface jar as a
  # jar compressed with jar
  rm -fr $TEST_TMPDIR/classes
  mkdir -p $TEST_TMPDIR/classes || fail "mkdir $TEST_TMPDIR/classes failed"
  $JAVAC -g -d $TEST_TMPDIR/classes $IJAR_SRCDIR/test/A.java ||
    fail "javac failed"
  $JAR cf $A_JAR $TEST_TMPDIR/classes/A.class || fail "jar failed"
  $ZIP $A_ZIP_JAR $TEST_TMPDIR/classes/A.class || fail "zip failed"

  $IJAR $A_JAR $A_INTERFACE_JAR || fail "ijar failed"
  $IJAR $A_ZIP_JAR $A_ZIP_INTERFACE_JAR || fail "ijar failed"
  cmp $A_INTERFACE_JAR $A_ZIP_INTERFACE_JAR || \
    fail "ijars from jar and zip are different"
}

function do_test_large_file() {
  # Compiles A.java, builds A.jar and A-interface.jar
  $JAVAC -g -d $TEST_TMPDIR/classes $IJAR_SRCDIR/test/A.java ||
    fail "javac failed"

  # First a check without large file to have something to compare to.
  for flag0 in '' '0'; do
    $JAR c"${flag0}"f $A_JAR -C $TEST_TMPDIR/classes . || fail "jar failed"
    $IJAR $A_JAR $A_INTERFACE_JAR || fail "ijar failed."
    check_consistent_file_contents $A_INTERFACE_JAR
  done

  # Then create larges files
  extra_args=""
  if [[ -n "${1-}" ]]; then
    for file in $(find $TEST_TMPDIR/classes -name '*.class'); do
      set_file_length "$file" "$1"
    done
  fi
  if [[ -n "${2-}" ]]; then
    set_file_length $TEST_TMPDIR/zeroes.data "$2"
    extra_args="-C $TEST_TMPDIR zeroes.data"
  fi

  for flag0 in '' '0'; do
    $JAR c"${flag0}"f $A_JAR $extra_args -C $TEST_TMPDIR/classes . || fail "jar failed"
    $IJAR $A_JAR $A_INTERFACE_JAR || fail "ijar failed."
    check_consistent_file_contents $A_INTERFACE_JAR
  done
}

function test_large_files() {
  # Ensure input files larger than INITIAL_BUFFER_SIZE work.
  # TODO(martinrb): remove maximum .class file size limit (MAX_BUFFER_SIZE)
  for size in $((1024*1024)) $((15*1024*1024)); do
      do_test_large_file $size
  done
}

# Create a huge (~2.2Gb) input jar to test "large file" correctness
function test_z_2gb_plus_data_file() {
  # This is slow because only writing a 2.2Gb file on a SSD drive is >10s and
  # jaring it takes >16s.
  # The z letter in the function name is to ensure that method is last in the
  # method list so it has more chance to be alone on a shard.
  do_test_large_file '' $((22*102*1024*1024))
}

# Create an output jar with upper bound on size > 2GB
function test_upper_bound_up_2gb() {
  DIR=$TEST_TMPDIR/ManyLargeClasses
  mkdir -p $DIR/classes
  for i in $(seq 200); do
    printf "class C${i} {}\n" > $DIR/C${i}.java
  done
  (cd $DIR && $JAVAC -d classes *.java)
  for i in $(seq 200); do
    set_file_length $DIR/classes/C${i}.class $((15*1024*1024))
  done
  $JAR cf $DIR/ManyLargeClasses.jar -C $DIR/classes . || fail "jar failed"
  $IJAR $DIR/ManyLargeClasses.jar $DIR/ManyLargeClasses.ijar || fail "ijar failed."
}

function test_empty_jar() {
  # Regression test for jar file without classes (javac doesn't like an empty ijar).
  >$TEST_TMPDIR/empty
  $ZIP $TEST_TMPDIR/noclasses.jar $TEST_TMPDIR/empty >/dev/null 2>&1
  $IJAR $TEST_TMPDIR/noclasses.jar || fail "ijar failed"
  $UNZIP -qql $TEST_TMPDIR/noclasses-interface.jar 2>/dev/null | grep -q . ||
    fail "noclasses-interface.jar is completely empty!"
  $JAVAC -classpath $TEST_TMPDIR/noclasses-interface.jar \
    -d $TEST_TMPDIR/classes \
    $IJAR_SRCDIR/test/A.java ||
    fail "javac noclasses-interface.jar failed"
  rm $TEST_TMPDIR/{empty,noclasses.jar,noclasses-interface.jar}
}

function test_wrong_centraldir() {
  # Check that a JAR file can be parsed even if the central directory file count
  # is wrong
  $IJAR $JAR_WRONG_CENTRAL_DIR $IJAR_WRONG_CENTRAL_DIR || fail "ijar failed"
  IJAR_FILES=$($UNZIP -qql $IJAR_WRONG_CENTRAL_DIR | wc -l | xargs echo)
  if [[ $IJAR_FILES != 2 ]]; then
    fail "ijar removed files"
  fi
}

function test_type_annotation() {
  # Check that constant pool references used by JSR308 type annotations are
  # preserved
  $IJAR $TYPEANN2_JAR $TYPEANN2_IJAR || fail "ijar failed"
  $JAVAP -classpath $TYPEANN2_IJAR -v Util >& $TEST_log || fail "javap failed"
  expect_log "RuntimeVisibleTypeAnnotations" "RuntimeVisibleTypeAnnotations not preserved!"
  cp $TYPEANN2_JAVA $TEST_TMPDIR/TypeAnnotationTest2.java
  $JAVAC -J-Xbootclasspath/p:$LANGTOOLS8 $TEST_TMPDIR/TypeAnnotationTest2.java -cp $TYPEANN2_IJAR ||
    fail "javac failed"
}

function test_invokedynamic() {
  # Check that ijar works on classes with invokedynamic
  $IJAR $INVOKEDYNAMIC_JAR $INVOKEDYNAMIC_IJAR || fail "ijar failed"
  lines=$($JAVAP -c -private -classpath $INVOKEDYNAMIC_JAR ClassWithLambda | grep Code: | wc -l)
  check_eq 4 $lines "Input jar should have 4 method bodies!"
  lines=$($JAVAP -c -private -classpath $INVOKEDYNAMIC_IJAR ClassWithLambda | grep Code: | wc -l)
  check_eq 0 $lines "Interface jar should have no method bodies!"
}

function test_object_class() {
  # Check that Object.class can be processed
  mkdir -p $TEST_TMPDIR/java/lang
  cp $OBJECT_JAVA $TEST_TMPDIR/java/lang/.
  $JAVAC $TEST_TMPDIR/java/lang/Object.java || fail "javac failed"
  $JAR cf $OBJECT_JAR -C $TEST_TMPDIR java/lang/Object.class || fail "jar failed"

  $IJAR $OBJECT_JAR $OBJECT_IJAR || fail "ijar failed"
}

function test_corrupted_end_of_centraldir() {
  # Check that the tool detects and reports a corrupted end of central directory
  # record condition
  CORRUPTED_JAR=$TEST_TMPDIR/corrupted.jar

  # First make the jar one byte longer
  cp $JAR_WRONG_CENTRAL_DIR $CORRUPTED_JAR
  chmod +w $CORRUPTED_JAR
  echo >> $CORRUPTED_JAR
  echo "Abort trap is expected"  # Show on the log that we expect failure.
  $IJAR $CORRUPTED_JAR 2> $TEST_log && fail "ijar should have failed" || status=$?
  check_ne 0 $status
  expect_log "missing end of central directory record"

  # Then make the jar one byte shorter than the original one
  let "NEW_SIZE = `statfmt $CORRUPTED_JAR` - 2"
  set_file_length $CORRUPTED_JAR $NEW_SIZE
  $IJAR $CORRUPTED_JAR 2> $TEST_log && fail "ijar should have failed" || status=$?
  check_ne 0 $status
  expect_log "missing end of central directory record"
}

function test_inner_class_argument() {
  cd $TEST_TMPDIR

  mkdir -p a b c
  cat > a/A.java <<EOF
package a;

public class A {
  public static class A2 {
    public int n;
  }
}
EOF

  cat > b/B.java <<EOF
package b;
import a.A;

public class B {
  public static void b(A.A2 arg) {
    System.out.println(arg.n);
  }
}
EOF

  cat > c/C.java <<EOF
package c;
import b.B;

public class C {
  public static void c() {
    B.b(null);
  }
}
EOF

  $JAVAC a/A.java b/B.java
  $JAR cf lib.jar {a,b}/*.class
  $JAVAC -cp lib.jar c/C.java

}

function test_inner_class_pruning() {
  cd $TEST_TMPDIR

  mkdir -p lib/l {one,two,three}/a

  cat > lib/l/L.java <<EOF
package l;

public class L {
  public static class I {
    public static class J {
      public static int number() {
        return 3;
      }
    }
    public static int number() {
      return 2;
    }
  }
}
EOF

  cat > one/a/A.java <<EOF
package a;

public class A {
  public static void message() {
    System.out.println("hello " + 1);
  }
}
EOF

  cat > two/a/A.java <<EOF
package a;

import l.L;

public class A {
  public static void message() {
    System.out.println("hello " + L.I.number());
  }
}
EOF

  cat > three/a/A.java <<EOF
package a;

import l.L;

public class A {
  public static void message() {
    System.out.println("hello " + L.I.J.number());
  }
}
EOF

  $JAVAC lib/l/L.java
  (cd lib; $JAR cf lib.jar l/*.class)
  $JAVAC one/a/A.java
  (cd one; $JAR cf one.jar a/*.class)
  $JAVAC two/a/A.java -classpath lib/lib.jar
  (cd two; $JAR cf two.jar a/*.class)
  $JAVAC three/a/A.java -classpath lib/lib.jar
  (cd three; $JAR cf three.jar a/*.class)

  $IJAR one/one.jar one/one-ijar.jar
  $IJAR one/one.jar two/two-ijar.jar
  $IJAR one/one.jar three/three-ijar.jar

  cmp one/one-ijar.jar two/two-ijar.jar
  cmp one/one-ijar.jar three/three-ijar.jar
}

function test_method_parameters_attribute() {
  # Check that Java 8 MethodParameters attributes are preserved
  $IJAR $METHODPARAM_JAR $METHODPARAM_IJAR || fail "ijar failed"
  $JAVAP -classpath $METHODPARAM_IJAR -v methodparameters.Test >& $TEST_log \
    || fail "javap failed"
  expect_log "MethodParameters" "MethodParameters not preserved!"
}

function test_source_debug_extension_attribute() {
  # Check that SourceDebugExtension attributes are dropped without a warning
  $IJAR $SOURCEDEBUGEXT_JAR $SOURCEDEBUGEXT_IJAR >& $TEST_log || fail "ijar failed"
  expect_not_log "skipping unknown attribute"
  $JAVAP -classpath $SOURCEDEBUGEXT_IJAR -v sourcedebugextension.Test >& $TEST_log \
    || fail "javap failed"
  expect_not_log "SourceDebugExtension" "SourceDebugExtension preserved!"
}

function test_central_dir_largest_regular() {
  $IJAR $CENTRAL_DIR_LARGEST_REGULAR $TEST_TMPDIR/ijar.jar || fail "ijar failed"
  $ZIP_COUNT $TEST_TMPDIR/ijar.jar 65535 || fail
}

function test_central_dir_smallest_zip64() {
  $IJAR $CENTRAL_DIR_SMALLEST_ZIP64 $TEST_TMPDIR/ijar.jar || fail "ijar failed"
  $ZIP_COUNT $TEST_TMPDIR/ijar.jar 65536 || fail
}

function test_central_dir_zip64() {
  $IJAR $CENTRAL_DIR_ZIP64 $TEST_TMPDIR/ijar.jar || fail "ijar failed"
  $ZIP_COUNT $TEST_TMPDIR/ijar.jar 70000 || fail
}

run_suite "ijar tests"
