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 : }
|