LCOV - code coverage report
Current view: top level - server/plugins - JarScanner.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 28 121 23.1 %
Date: 2022-11-19 15:00:39 Functions: 6 37 16.2 %

          Line data    Source code
       1             : // Copyright (C) 2014 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.plugins;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.common.collect.Iterables.transform;
      20             : 
      21             : import com.google.common.base.Strings;
      22             : import com.google.common.collect.ImmutableMap;
      23             : import com.google.common.collect.ListMultimap;
      24             : import com.google.common.collect.Maps;
      25             : import com.google.common.collect.MultimapBuilder;
      26             : import com.google.common.flogger.FluentLogger;
      27             : import com.google.gerrit.common.Nullable;
      28             : import java.io.IOException;
      29             : import java.io.InputStream;
      30             : import java.lang.annotation.Annotation;
      31             : import java.nio.file.Path;
      32             : import java.util.ArrayList;
      33             : import java.util.Collection;
      34             : import java.util.Collections;
      35             : import java.util.HashMap;
      36             : import java.util.HashSet;
      37             : import java.util.List;
      38             : import java.util.Map;
      39             : import java.util.Optional;
      40             : import java.util.Set;
      41             : import java.util.jar.Attributes;
      42             : import java.util.jar.JarEntry;
      43             : import java.util.jar.JarFile;
      44             : import java.util.jar.Manifest;
      45             : import java.util.stream.Stream;
      46             : import org.eclipse.jgit.util.IO;
      47             : import org.objectweb.asm.AnnotationVisitor;
      48             : import org.objectweb.asm.Attribute;
      49             : import org.objectweb.asm.ClassReader;
      50             : import org.objectweb.asm.ClassVisitor;
      51             : import org.objectweb.asm.FieldVisitor;
      52             : import org.objectweb.asm.MethodVisitor;
      53             : import org.objectweb.asm.Opcodes;
      54             : import org.objectweb.asm.Type;
      55             : 
      56             : public class JarScanner implements PluginContentScanner, AutoCloseable {
      57           1 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      58             : 
      59             :   private static final int SKIP_ALL =
      60             :       ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
      61             :   private final JarFile jarFile;
      62             : 
      63           1 :   public JarScanner(Path src) throws IOException {
      64           1 :     this.jarFile = new JarFile(src.toFile());
      65           1 :   }
      66             : 
      67             :   @Override
      68             :   public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
      69             :       String pluginName, Iterable<Class<? extends Annotation>> annotations)
      70             :       throws InvalidPluginException {
      71           1 :     Set<String> descriptors = new HashSet<>();
      72             :     ListMultimap<String, JarScanner.ClassData> rawMap =
      73           1 :         MultimapBuilder.hashKeys().arrayListValues().build();
      74           1 :     Map<Class<? extends Annotation>, String> classObjToClassDescr = new HashMap<>();
      75             : 
      76           1 :     for (Class<? extends Annotation> annotation : annotations) {
      77           1 :       String descriptor = Type.getType(annotation).getDescriptor();
      78           1 :       descriptors.add(descriptor);
      79           1 :       classObjToClassDescr.put(annotation, descriptor);
      80           1 :     }
      81             : 
      82           1 :     for (JarEntry entry : entriesOf(jarFile)) {
      83           1 :       if (skip(entry)) {
      84           1 :         continue;
      85             :       }
      86             : 
      87           0 :       ClassData def = new ClassData(descriptors);
      88             :       try {
      89           0 :         new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
      90           0 :       } catch (IOException err) {
      91           0 :         throw new InvalidPluginException("Cannot auto-register", err);
      92           0 :       } catch (RuntimeException err) {
      93           0 :         logger.atWarning().withCause(err).log(
      94             :             "Plugin %s has invalid class file %s inside of %s",
      95           0 :             pluginName, entry.getName(), jarFile.getName());
      96           0 :         continue;
      97           0 :       }
      98             : 
      99           0 :       if (!Strings.isNullOrEmpty(def.annotationName)) {
     100           0 :         if (def.isConcrete()) {
     101           0 :           rawMap.put(def.annotationName, def);
     102             :         } else {
     103           0 :           logger.atWarning().log(
     104             :               "Plugin %s tries to @%s(\"%s\") abstract class %s",
     105             :               pluginName, def.annotationName, def.annotationValue, def.className);
     106             :         }
     107             :       }
     108           0 :     }
     109             : 
     110             :     ImmutableMap.Builder<Class<? extends Annotation>, Iterable<ExtensionMetaData>> result =
     111           1 :         ImmutableMap.builder();
     112             : 
     113           1 :     for (Class<? extends Annotation> annotoation : annotations) {
     114           1 :       String descr = classObjToClassDescr.get(annotoation);
     115           1 :       Collection<ClassData> discoverdData = rawMap.get(descr);
     116           1 :       Collection<ClassData> values = firstNonNull(discoverdData, Collections.emptySet());
     117             : 
     118           1 :       result.put(
     119             :           annotoation,
     120           1 :           transform(values, cd -> new ExtensionMetaData(cd.className, cd.annotationValue)));
     121           1 :     }
     122             : 
     123           1 :     return result.build();
     124             :   }
     125             : 
     126             :   public List<String> findSubClassesOf(Class<?> superClass) throws IOException {
     127           0 :     return findSubClassesOf(superClass.getName());
     128             :   }
     129             : 
     130             :   @Override
     131             :   public void close() throws IOException {
     132           0 :     jarFile.close();
     133           0 :   }
     134             : 
     135             :   private List<String> findSubClassesOf(String superClass) throws IOException {
     136           0 :     String name = superClass.replace('.', '/');
     137             : 
     138           0 :     List<String> classes = new ArrayList<>();
     139           0 :     for (JarEntry entry : entriesOf(jarFile)) {
     140           0 :       if (skip(entry)) {
     141           0 :         continue;
     142             :       }
     143             : 
     144           0 :       ClassData def = new ClassData(Collections.emptySet());
     145             :       try {
     146           0 :         new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
     147           0 :       } catch (RuntimeException err) {
     148           0 :         logger.atWarning().withCause(err).log(
     149           0 :             "Jar %s has invalid class file %s", jarFile.getName(), entry.getName());
     150           0 :         continue;
     151           0 :       }
     152             : 
     153           0 :       if (name.equals(def.superName)) {
     154           0 :         classes.addAll(findSubClassesOf(def.className));
     155           0 :         if (def.isConcrete()) {
     156           0 :           classes.add(def.className);
     157             :         }
     158             :       }
     159           0 :     }
     160             : 
     161           0 :     return classes;
     162             :   }
     163             : 
     164             :   private static boolean skip(JarEntry entry) {
     165           1 :     if (!entry.getName().endsWith(".class")) {
     166           1 :       return true; // Avoid non-class resources.
     167             :     }
     168           0 :     if (entry.getSize() <= 0) {
     169           0 :       return true; // Directories have 0 size.
     170             :     }
     171           0 :     if (entry.getSize() >= 1024 * 1024) {
     172           0 :       return true; // Do not scan huge class files.
     173             :     }
     174           0 :     return false;
     175             :   }
     176             : 
     177             :   private static byte[] read(JarFile jarFile, JarEntry entry) throws IOException {
     178           0 :     byte[] data = new byte[(int) entry.getSize()];
     179           0 :     try (InputStream in = jarFile.getInputStream(entry)) {
     180           0 :       IO.readFully(in, data, 0, data.length);
     181             :     }
     182           0 :     return data;
     183             :   }
     184             : 
     185             :   public static class ClassData extends ClassVisitor {
     186             :     int access;
     187             :     String className;
     188             :     String superName;
     189             :     String annotationName;
     190             :     String annotationValue;
     191             :     String[] interfaces;
     192             :     Collection<String> exports;
     193             : 
     194             :     private ClassData(Collection<String> exports) {
     195           0 :       super(Opcodes.ASM7);
     196           0 :       this.exports = exports;
     197           0 :     }
     198             : 
     199             :     boolean isConcrete() {
     200           0 :       return (access & Opcodes.ACC_ABSTRACT) == 0 && (access & Opcodes.ACC_INTERFACE) == 0;
     201             :     }
     202             : 
     203             :     @Override
     204             :     public void visit(
     205             :         int version,
     206             :         int access,
     207             :         String name,
     208             :         String signature,
     209             :         String superName,
     210             :         String[] interfaces) {
     211           0 :       this.className = Type.getObjectType(name).getClassName();
     212           0 :       this.access = access;
     213           0 :       this.superName = superName;
     214           0 :     }
     215             : 
     216             :     @Nullable
     217             :     @Override
     218             :     public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
     219           0 :       if (!visible) {
     220           0 :         return null;
     221             :       }
     222           0 :       Optional<String> found = exports.stream().filter(x -> x.equals(desc)).findAny();
     223           0 :       if (found.isPresent()) {
     224           0 :         annotationName = desc;
     225           0 :         return new AbstractAnnotationVisitor() {
     226             :           @Override
     227             :           public void visit(String name, Object value) {
     228           0 :             annotationValue = (String) value;
     229           0 :           }
     230             :         };
     231             :       }
     232           0 :       return null;
     233             :     }
     234             : 
     235             :     @Override
     236           0 :     public void visitSource(String arg0, String arg1) {}
     237             : 
     238             :     @Override
     239           0 :     public void visitOuterClass(String arg0, String arg1, String arg2) {}
     240             : 
     241             :     @Override
     242             :     public MethodVisitor visitMethod(
     243             :         int arg0, String arg1, String arg2, String arg3, String[] arg4) {
     244           0 :       return null;
     245             :     }
     246             : 
     247             :     @Override
     248           0 :     public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {}
     249             : 
     250             :     @Override
     251             :     public FieldVisitor visitField(int arg0, String arg1, String arg2, String arg3, Object arg4) {
     252           0 :       return null;
     253             :     }
     254             : 
     255             :     @Override
     256           0 :     public void visitEnd() {}
     257             : 
     258             :     @Override
     259           0 :     public void visitAttribute(Attribute arg0) {}
     260             :   }
     261             : 
     262             :   private abstract static class AbstractAnnotationVisitor extends AnnotationVisitor {
     263             :     AbstractAnnotationVisitor() {
     264           0 :       super(Opcodes.ASM7);
     265           0 :     }
     266             : 
     267             :     @Override
     268             :     public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
     269           0 :       return null;
     270             :     }
     271             : 
     272             :     @Override
     273             :     public AnnotationVisitor visitArray(String arg0) {
     274           0 :       return null;
     275             :     }
     276             : 
     277             :     @Override
     278           0 :     public void visitEnum(String arg0, String arg1, String arg2) {}
     279             : 
     280             :     @Override
     281           0 :     public void visitEnd() {}
     282             :   }
     283             : 
     284             :   @Override
     285             :   public Optional<PluginEntry> getEntry(String resourcePath) throws IOException {
     286           0 :     JarEntry jarEntry = jarFile.getJarEntry(resourcePath);
     287           0 :     if (jarEntry == null || jarEntry.getSize() == 0) {
     288           0 :       return Optional.empty();
     289             :     }
     290             : 
     291           0 :     return Optional.of(resourceOf(jarEntry));
     292             :   }
     293             : 
     294             :   @Override
     295             :   public Stream<PluginEntry> entries() {
     296           0 :     return jarFile.stream()
     297           0 :         .map(
     298             :             jarEntry -> {
     299             :               try {
     300           0 :                 return resourceOf(jarEntry);
     301           0 :               } catch (IOException e) {
     302           0 :                 throw new IllegalArgumentException(
     303             :                     "Cannot convert jar entry " + jarEntry + " to a resource", e);
     304             :               }
     305             :             });
     306             :   }
     307             : 
     308             :   @Override
     309             :   public InputStream getInputStream(PluginEntry entry) throws IOException {
     310           0 :     return jarFile.getInputStream(jarFile.getEntry(entry.getName()));
     311             :   }
     312             : 
     313             :   @Override
     314             :   public Manifest getManifest() throws IOException {
     315           1 :     return jarFile.getManifest();
     316             :   }
     317             : 
     318             :   private PluginEntry resourceOf(JarEntry jarEntry) throws IOException {
     319           0 :     return new PluginEntry(
     320           0 :         jarEntry.getName(),
     321           0 :         jarEntry.getTime(),
     322           0 :         Optional.of(jarEntry.getSize()),
     323           0 :         attributesOf(jarEntry));
     324             :   }
     325             : 
     326             :   private Map<Object, String> attributesOf(JarEntry jarEntry) throws IOException {
     327           0 :     Attributes attributes = jarEntry.getAttributes();
     328           0 :     if (attributes == null) {
     329           0 :       return Collections.emptyMap();
     330             :     }
     331           0 :     return Maps.transformEntries(attributes, (key, value) -> (String) value);
     332             :   }
     333             : 
     334             :   private static Iterable<JarEntry> entriesOf(JarFile jarFile) {
     335           1 :     return jarFile.stream().collect(toImmutableList());
     336             :   }
     337             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750