content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 org.apache.cassandra.simulator.asm;
  
  import java.util.EnumSet;
  import java.util.List;
  import java.util.function.Consumer;
  
  import org.objectweb.asm.AnnotationVisitor;
  import org.objectweb.asm.ClassReader;
  import org.objectweb.asm.ClassVisitor;
  import org.objectweb.asm.ClassWriter;
  import org.objectweb.asm.FieldVisitor;
  import org.objectweb.asm.Handle;
  import org.objectweb.asm.Label;
  import org.objectweb.asm.MethodVisitor;
  import org.objectweb.asm.Opcodes;
  import org.objectweb.asm.tree.AbstractInsnNode;
  import org.objectweb.asm.tree.MethodInsnNode;
  import org.objectweb.asm.tree.MethodNode;
  
  import static java.util.Collections.singletonList;
  import static org.apache.cassandra.simulator.asm.Flag.DETERMINISTIC;
  import static org.apache.cassandra.simulator.asm.Flag.GLOBAL_METHODS;
  import static org.apache.cassandra.simulator.asm.Flag.MONITORS;
  import static org.apache.cassandra.simulator.asm.Flag.NEMESIS;
  import static org.apache.cassandra.simulator.asm.Flag.NO_PROXY_METHODS;
  import static org.apache.cassandra.simulator.asm.TransformationKind.HASHCODE;
  import static org.apache.cassandra.simulator.asm.TransformationKind.SYNCHRONIZED;
  import static org.apache.cassandra.simulator.asm.Utils.deterministicToString;
  import static org.apache.cassandra.simulator.asm.Utils.generateTryFinallyProxyCall;
  import static org.apache.cassandra.simulator.asm.Utils.visitEachRefType;
  import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
  import static org.objectweb.asm.Opcodes.ACC_STATIC;
  import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
  import static org.objectweb.asm.Opcodes.INVOKESTATIC;
  
  class ClassTransformer extends ClassVisitor implements MethodWriterSink
  {
      private static final List<AbstractInsnNode> DETERMINISM_SETUP = singletonList(new MethodInsnNode(INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptibleThread", "enterDeterministicMethod", "()V", false));
      private static final List<AbstractInsnNode> DETERMINISM_CLEANUP = singletonList(new MethodInsnNode(INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptibleThread", "exitDeterministicMethod", "()V", false));
  
      class DependentTypeVisitor extends MethodVisitor
      {
          public DependentTypeVisitor(int api, MethodVisitor methodVisitor)
          {
              super(api, methodVisitor);
          }
  
          @Override
          public void visitTypeInsn(int opcode, String type)
          {
              super.visitTypeInsn(opcode, type);
              Utils.visitIfRefType(type, dependentTypes);
          }
  
          @Override
          public void visitFieldInsn(int opcode, String owner, String name, String descriptor)
          {
              super.visitFieldInsn(opcode, owner, name, descriptor);
              Utils.visitIfRefType(descriptor, dependentTypes);
          }
  
          @Override
          public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface)
          {
              super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
              Utils.visitEachRefType(descriptor, dependentTypes);
          }
  
          @Override
          public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments)
          {
              super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
              Utils.visitEachRefType(descriptor, dependentTypes);
          }
  
          @Override
          public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index)
          {
              super.visitLocalVariable(name, descriptor, signature, start, end, index);
              Utils.visitIfRefType(descriptor, dependentTypes);
          }
      }
  
      private final String className;
      private final ChanceSupplier monitorDelayChance;
      private final NemesisGenerator nemesis;
      private final NemesisFieldKind.Selector nemesisFieldSelector;
      private final Hashcode insertHashcode;
      private final MethodLogger methodLogger;
      private boolean isTransformed;
      private boolean isCacheablyTransformed = true;
      private final EnumSet<Flag> flags;
      private final Consumer<String> dependentTypes;
  
      private boolean updateVisibility = false;
  
      ClassTransformer(int api, String className, EnumSet<Flag> flags, Consumer<String> dependentTypes)
      {
          this(api, new ClassWriter(0), className, flags, null, null, null, null, dependentTypes);
      }
  
      ClassTransformer(int api, String className, EnumSet<Flag> flags, ChanceSupplier monitorDelayChance, NemesisGenerator nemesis, NemesisFieldKind.Selector nemesisFieldSelector, Hashcode insertHashcode, Consumer<String> dependentTypes)
      {
          this(api, new ClassWriter(0), className, flags, monitorDelayChance, nemesis, nemesisFieldSelector, insertHashcode, dependentTypes);
      }
  
      private ClassTransformer(int api, ClassWriter classWriter, String className, EnumSet<Flag> flags, ChanceSupplier monitorDelayChance, NemesisGenerator nemesis, NemesisFieldKind.Selector nemesisFieldSelector, Hashcode insertHashcode, Consumer<String> dependentTypes)
      {
          super(api, classWriter);
          if (flags.contains(NEMESIS) && (nemesis == null || nemesisFieldSelector == null))
              throw new IllegalArgumentException();
          if (flags.contains(MONITORS) && monitorDelayChance == null)
              throw new IllegalArgumentException();
          this.dependentTypes = dependentTypes;
          this.className = className;
          this.flags = flags;
          this.monitorDelayChance = monitorDelayChance;
          this.nemesis = nemesis;
          this.nemesisFieldSelector = nemesisFieldSelector;
          this.insertHashcode = insertHashcode;
          this.methodLogger = MethodLogger.log(api, className);
      }
  
      public void setUpdateVisibility(boolean updateVisibility)
      {
          this.updateVisibility = updateVisibility;
      }
  
      /**
       * Java 11 changed the way that classes defined in the same source file get access to private state (see https://openjdk.org/jeps/181),
       * rather than trying to adapt to this, this method attempts to make the field/method/class public so that access
       * is not restricted.
       */
      private int makePublic(int access)
      {
          if (!updateVisibility)
              return access;
          // leave non-user created methods/fields/etc. alone
          if (contains(access, Opcodes.ACC_BRIDGE) || contains(access, Opcodes.ACC_SYNTHETIC))
              return access;
          if (contains(access, Opcodes.ACC_PRIVATE))
          {
              access &= ~Opcodes.ACC_PRIVATE;
              access |= Opcodes.ACC_PUBLIC;
          }
          else if (contains(access, Opcodes.ACC_PROTECTED))
          {
              access &= ~Opcodes.ACC_PROTECTED;
              access |= Opcodes.ACC_PUBLIC;
          }
          else if (!contains(access, Opcodes.ACC_PUBLIC)) // package-protected
          {
              access |= Opcodes.ACC_PUBLIC;
          }
          return access;
      }
  
      private static boolean contains(int value, int mask)
      {
          return (value & mask) != 0;
      }
  
      @Override
      public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
      {
          super.visit(version, makePublic(access), name, signature, superName, interfaces);
      }
  
      @Override
      public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value)
      {
          if (dependentTypes != null)
              Utils.visitIfRefType(descriptor, dependentTypes);
          // org.apache.cassandra.simulator.systems.SimulatedTime.InstanceTime.nanoTime does not change between invokes which causes AbstractQueuedSynchronizer to loop forever,
          // so need to make the threshold negative to avoid the spin loop.
          if (className.equals("java/util/concurrent/locks/AbstractQueuedSynchronizer") && name.equals("SPIN_FOR_TIMEOUT_THRESHOLD"))
              return super.visitField(makePublic(access), name, descriptor, signature, Long.MIN_VALUE);
          return super.visitField(makePublic(access), name, descriptor, signature, value);
      }
  
      @Override
      public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
      {
          if (dependentTypes != null)
              visitEachRefType(descriptor, dependentTypes);
  
          EnumSet<Flag> flags = this.flags;
          if (flags.isEmpty() || ((access & ACC_SYNTHETIC) != 0 && (name.endsWith("$unsync") || name.endsWith("$catch") || name.endsWith("$nemesis"))))
          {
              MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
              if (dependentTypes != null && (access & (ACC_STATIC | ACC_SYNTHETIC)) != 0 && (name.equals("<clinit>") || name.startsWith("lambda$")))
                  visitor = new DependentTypeVisitor(api, visitor);
              return visitor;
          }
  
          boolean isToString = false;
          if (access == Opcodes.ACC_PUBLIC && name.equals("toString") && descriptor.equals("()Ljava/lang/String;") && !flags.contains(NO_PROXY_METHODS))
          {
              generateTryFinallyProxyCall(super.visitMethod(access, name, descriptor, signature, exceptions), className,
                                          "toString$original", "()Ljava/lang/String;", access, true, false, DETERMINISM_SETUP, DETERMINISM_CLEANUP);
              access = ACC_PRIVATE | ACC_SYNTHETIC;
              name = "toString$original";
              if (!flags.contains(DETERMINISTIC) || flags.contains(NEMESIS))
              {
                  flags = EnumSet.copyOf(flags);
                  flags.add(DETERMINISTIC);
                  flags.remove(NEMESIS);
              }
              isToString = true;
          }
  
          access = makePublic(access);
          MethodVisitor visitor;
          if (flags.contains(MONITORS) && (access & Opcodes.ACC_SYNCHRONIZED) != 0)
          {
              visitor = new MonitorMethodTransformer(this, className, api, access, name, descriptor, signature, exceptions, monitorDelayChance);
              witness(SYNCHRONIZED);
          }
          else
          {
              visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
              visitor = methodLogger.visitMethod(access, name, descriptor, visitor);
          }
  
          if (flags.contains(MONITORS))
              visitor = new MonitorEnterExitParkTransformer(this, api, visitor, className, monitorDelayChance);
          if (isToString)
              visitor = deterministicToString(visitor);
          if (flags.contains(GLOBAL_METHODS) || flags.contains(Flag.LOCK_SUPPORT) || flags.contains(Flag.DETERMINISTIC))
              visitor = new GlobalMethodTransformer(flags, this, api, name, visitor);
          if (flags.contains(NEMESIS))
              visitor = new NemesisTransformer(this, api, name, visitor, nemesis, nemesisFieldSelector);
          if (dependentTypes != null && (access & (ACC_STATIC | ACC_SYNTHETIC)) != 0 && (name.equals("<clinit>") || name.startsWith("lambda$")))
              visitor = new DependentTypeVisitor(api, visitor);
          return visitor;
      }
  
      @Override
      public void visitEnd()
      {
          if (insertHashcode != null)
              writeSyntheticMethod(HASHCODE, insertHashcode);
          super.visitEnd();
          methodLogger.visitEndOfClass();
      }
  
      public void writeMethod(MethodNode node)
      {
          writeMethod(null, node);
      }
  
      public void writeSyntheticMethod(TransformationKind kind, MethodNode node)
      {
          writeMethod(kind, node);
      }
  
      void writeMethod(TransformationKind kind, MethodNode node)
      {
          String[] exceptions = node.exceptions == null ? null : node.exceptions.toArray(new String[0]);
          MethodVisitor visitor = super.visitMethod(node.access, node.name, node.desc, node.signature, exceptions);
          visitor = methodLogger.visitMethod(node.access, node.name, node.desc, visitor);
          if (kind != null)
              witness(kind);
          node.accept(visitor);
      }
  
      @Override
      public AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
      {
          return Utils.checkForSimulationAnnotations(api, descriptor, super.visitAnnotation(descriptor, visible), (flag, add) -> {
              if (add) flags.add(flag);
              else flags.remove(flag);
          });
      }
  
      void readAndTransform(byte[] input)
      {
          ClassReader reader = new ClassReader(input);
          reader.accept(this, 0);
      }
  
      void witness(TransformationKind kind)
      {
          isTransformed = true;
          switch (kind)
          {
              case FIELD_NEMESIS:
              case SIGNAL_NEMESIS:
                  // TODO: this isn't correct: we will share any class we choose not to insert nemesis points into on first transformation
                  isCacheablyTransformed = false;
          }
          methodLogger.witness(kind);
      }
  
      String className()
      {
          return className;
      }
  
      boolean isTransformed()
      {
          return isTransformed;
      }
  
      boolean isCacheablyTransformed()
      {
          return isCacheablyTransformed;
      }
  
      byte[] toBytes()
      {
          return ((ClassWriter) cv).toByteArray();
      }
  }
  