1 /******************************************************************************* 2 * Copyright (c) 2009, 2021 Mountainminds GmbH & Co. KG and Contributors 3 * This program and the accompanying materials are made available under 4 * the terms of the Eclipse Public License 2.0 which is available at 5 * http://www.eclipse.org/legal/epl-2.0 6 * 7 * SPDX-License-Identifier: EPL-2.0 8 * 9 * Contributors: 10 * Marc R. Hoffmann - initial API and implementation 11 * 12 *******************************************************************************/ 13 package org.jacoco.core.instr; 14 15 import java.io.ByteArrayOutputStream; 16 import java.io.IOException; 17 import java.io.InputStream; 18 import java.io.OutputStream; 19 import java.util.zip.CRC32; 20 import java.util.zip.GZIPInputStream; 21 import java.util.zip.GZIPOutputStream; 22 import java.util.zip.ZipEntry; 23 import java.util.zip.ZipInputStream; 24 import java.util.zip.ZipOutputStream; 25 26 import org.jacoco.core.internal.ContentTypeDetector; 27 import org.jacoco.core.internal.InputStreams; 28 import org.jacoco.core.internal.Pack200Streams; 29 import org.jacoco.core.internal.data.CRC64; 30 import org.jacoco.core.internal.flow.ClassProbesAdapter; 31 import org.jacoco.core.internal.instr.ClassInstrumenter; 32 import org.jacoco.core.internal.instr.IProbeArrayStrategy; 33 import org.jacoco.core.internal.instr.InstrSupport; 34 import org.jacoco.core.internal.instr.ProbeArrayStrategyFactory; 35 import org.jacoco.core.internal.instr.SignatureRemover; 36 import org.jacoco.core.runtime.IExecutionDataAccessorGenerator; 37 import org.objectweb.asm.ClassReader; 38 import org.objectweb.asm.ClassVisitor; 39 import org.objectweb.asm.ClassWriter; 40 41 /** 42 * Several APIs to instrument Java class definitions for coverage tracing. 43 */ 44 public class Instrumenter { 45 46 private final IExecutionDataAccessorGenerator accessorGenerator; 47 48 private final SignatureRemover signatureRemover; 49 50 /** 51 * Creates a new instance based on the given runtime. 52 * 53 * @param runtime 54 * runtime used by the instrumented classes 55 */ Instrumenter(final IExecutionDataAccessorGenerator runtime)56 public Instrumenter(final IExecutionDataAccessorGenerator runtime) { 57 this.accessorGenerator = runtime; 58 this.signatureRemover = new SignatureRemover(); 59 } 60 61 /** 62 * Determines whether signatures should be removed from JAR files. This is 63 * typically necessary as instrumentation modifies the class files and 64 * therefore invalidates existing JAR signatures. Default is 65 * <code>true</code>. 66 * 67 * @param flag 68 * <code>true</code> if signatures should be removed 69 */ setRemoveSignatures(final boolean flag)70 public void setRemoveSignatures(final boolean flag) { 71 signatureRemover.setActive(flag); 72 } 73 instrument(final byte[] source)74 private byte[] instrument(final byte[] source) { 75 final long classId = CRC64.classId(source); 76 final ClassReader reader = InstrSupport.classReaderFor(source); 77 final ClassWriter writer = new ClassWriter(reader, 0) { 78 @Override 79 protected String getCommonSuperClass(final String type1, 80 final String type2) { 81 throw new IllegalStateException(); 82 } 83 }; 84 final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory 85 .createFor(classId, reader, accessorGenerator); 86 final int version = InstrSupport.getMajorVersion(reader); 87 final ClassVisitor visitor = new ClassProbesAdapter( 88 new ClassInstrumenter(strategy, writer), 89 InstrSupport.needsFrames(version)); 90 reader.accept(visitor, ClassReader.EXPAND_FRAMES); 91 return writer.toByteArray(); 92 } 93 94 /** 95 * Creates a instrumented version of the given class if possible. 96 * 97 * @param buffer 98 * definition of the class 99 * @param name 100 * a name used for exception messages 101 * @return instrumented definition 102 * @throws IOException 103 * if the class can't be instrumented 104 */ instrument(final byte[] buffer, final String name)105 public byte[] instrument(final byte[] buffer, final String name) 106 throws IOException { 107 try { 108 return instrument(buffer); 109 } catch (final RuntimeException e) { 110 throw instrumentError(name, e); 111 } 112 } 113 114 /** 115 * Creates a instrumented version of the given class if possible. The 116 * provided {@link InputStream} is not closed by this method. 117 * 118 * @param input 119 * stream to read class definition from 120 * @param name 121 * a name used for exception messages 122 * @return instrumented definition 123 * @throws IOException 124 * if reading data from the stream fails or the class can't be 125 * instrumented 126 */ instrument(final InputStream input, final String name)127 public byte[] instrument(final InputStream input, final String name) 128 throws IOException { 129 final byte[] bytes; 130 try { 131 bytes = InputStreams.readFully(input); 132 } catch (final IOException e) { 133 throw instrumentError(name, e); 134 } 135 return instrument(bytes, name); 136 } 137 138 /** 139 * Creates a instrumented version of the given class file. The provided 140 * {@link InputStream} and {@link OutputStream} instances are not closed by 141 * this method. 142 * 143 * @param input 144 * stream to read class definition from 145 * @param output 146 * stream to write the instrumented version of the class to 147 * @param name 148 * a name used for exception messages 149 * @throws IOException 150 * if reading data from the stream fails or the class can't be 151 * instrumented 152 */ instrument(final InputStream input, final OutputStream output, final String name)153 public void instrument(final InputStream input, final OutputStream output, 154 final String name) throws IOException { 155 output.write(instrument(input, name)); 156 } 157 instrumentError(final String name, final Exception cause)158 private IOException instrumentError(final String name, 159 final Exception cause) { 160 final IOException ex = new IOException( 161 String.format("Error while instrumenting %s.", name)); 162 ex.initCause(cause); 163 return ex; 164 } 165 166 /** 167 * Creates a instrumented version of the given resource depending on its 168 * type. Class files and the content of archive files are instrumented. All 169 * other files are copied without modification. The provided 170 * {@link InputStream} and {@link OutputStream} instances are not closed by 171 * this method. 172 * 173 * @param input 174 * stream to contents from 175 * @param output 176 * stream to write the instrumented version of the contents 177 * @param name 178 * a name used for exception messages 179 * @return number of instrumented classes 180 * @throws IOException 181 * if reading data from the stream fails or a class can't be 182 * instrumented 183 */ instrumentAll(final InputStream input, final OutputStream output, final String name)184 public int instrumentAll(final InputStream input, final OutputStream output, 185 final String name) throws IOException { 186 final ContentTypeDetector detector; 187 try { 188 detector = new ContentTypeDetector(input); 189 } catch (final IOException e) { 190 throw instrumentError(name, e); 191 } 192 switch (detector.getType()) { 193 case ContentTypeDetector.CLASSFILE: 194 instrument(detector.getInputStream(), output, name); 195 return 1; 196 case ContentTypeDetector.ZIPFILE: 197 return instrumentZip(detector.getInputStream(), output, name); 198 case ContentTypeDetector.GZFILE: 199 return instrumentGzip(detector.getInputStream(), output, name); 200 case ContentTypeDetector.PACK200FILE: 201 return instrumentPack200(detector.getInputStream(), output, name); 202 default: 203 copy(detector.getInputStream(), output, name); 204 return 0; 205 } 206 } 207 instrumentZip(final InputStream input, final OutputStream output, final String name)208 private int instrumentZip(final InputStream input, 209 final OutputStream output, final String name) throws IOException { 210 final ZipInputStream zipin = new ZipInputStream(input); 211 final ZipOutputStream zipout = new ZipOutputStream(output); 212 ZipEntry entry; 213 int count = 0; 214 while ((entry = nextEntry(zipin, name)) != null) { 215 final String entryName = entry.getName(); 216 if (signatureRemover.removeEntry(entryName)) { 217 continue; 218 } 219 220 final ZipEntry newEntry = new ZipEntry(entryName); 221 newEntry.setMethod(entry.getMethod()); 222 switch (entry.getMethod()) { 223 case ZipEntry.DEFLATED: 224 zipout.putNextEntry(newEntry); 225 count += filterOrInstrument(zipin, zipout, name, entryName); 226 break; 227 case ZipEntry.STORED: 228 // Uncompressed entries must be processed in-memory to calculate 229 // mandatory entry size and CRC 230 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 231 count += filterOrInstrument(zipin, buffer, name, entryName); 232 final byte[] bytes = buffer.toByteArray(); 233 newEntry.setSize(bytes.length); 234 newEntry.setCompressedSize(bytes.length); 235 newEntry.setCrc(crc(bytes)); 236 zipout.putNextEntry(newEntry); 237 zipout.write(bytes); 238 break; 239 default: 240 throw new AssertionError(entry.getMethod()); 241 } 242 zipout.closeEntry(); 243 } 244 zipout.finish(); 245 return count; 246 } 247 filterOrInstrument(final InputStream in, final OutputStream out, final String name, final String entryName)248 private int filterOrInstrument(final InputStream in, final OutputStream out, 249 final String name, final String entryName) throws IOException { 250 if (signatureRemover.filterEntry(entryName, in, out)) { 251 return 0; 252 } else { 253 return instrumentAll(in, out, name + "@" + entryName); 254 } 255 } 256 crc(final byte[] data)257 private static long crc(final byte[] data) { 258 final CRC32 crc = new CRC32(); 259 crc.update(data); 260 return crc.getValue(); 261 } 262 nextEntry(final ZipInputStream input, final String location)263 private ZipEntry nextEntry(final ZipInputStream input, 264 final String location) throws IOException { 265 try { 266 return input.getNextEntry(); 267 } catch (final IOException e) { 268 throw instrumentError(location, e); 269 } 270 } 271 instrumentGzip(final InputStream input, final OutputStream output, final String name)272 private int instrumentGzip(final InputStream input, 273 final OutputStream output, final String name) throws IOException { 274 final GZIPInputStream gzipInputStream; 275 try { 276 gzipInputStream = new GZIPInputStream(input); 277 } catch (final IOException e) { 278 throw instrumentError(name, e); 279 } 280 final GZIPOutputStream gzout = new GZIPOutputStream(output); 281 final int count = instrumentAll(gzipInputStream, gzout, name); 282 gzout.finish(); 283 return count; 284 } 285 instrumentPack200(final InputStream input, final OutputStream output, final String name)286 private int instrumentPack200(final InputStream input, 287 final OutputStream output, final String name) throws IOException { 288 final InputStream unpackedInput; 289 try { 290 unpackedInput = Pack200Streams.unpack(input); 291 } catch (final IOException e) { 292 throw instrumentError(name, e); 293 } 294 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 295 final int count = instrumentAll(unpackedInput, buffer, name); 296 Pack200Streams.pack(buffer.toByteArray(), output); 297 return count; 298 } 299 copy(final InputStream input, final OutputStream output, final String name)300 private void copy(final InputStream input, final OutputStream output, 301 final String name) throws IOException { 302 final byte[] buffer = new byte[1024]; 303 int len; 304 while ((len = read(input, buffer, name)) != -1) { 305 output.write(buffer, 0, len); 306 } 307 } 308 read(final InputStream input, final byte[] buffer, final String name)309 private int read(final InputStream input, final byte[] buffer, 310 final String name) throws IOException { 311 try { 312 return input.read(buffer); 313 } catch (final IOException e) { 314 throw instrumentError(name, e); 315 } 316 } 317 318 } 319