1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @summary Test zip compressor
  27  * @author Jean-Francois Denise
  28  * @modules java.base/jdk.internal.jimage.decompressor
  29  *          jdk.jlink/jdk.tools.jlink.internal
  30  *          jdk.jlink/jdk.tools.jlink.internal.plugins
  31  *          jdk.jlink/jdk.tools.jlink.plugin
  32  * @run main CompressorPluginTest
  33  */
  34 import java.net.URI;
  35 import java.nio.ByteOrder;
  36 import java.nio.file.FileSystem;
  37 import java.nio.file.FileSystemNotFoundException;
  38 import java.nio.file.FileSystems;
  39 import java.nio.file.Files;
  40 import java.nio.file.Path;
  41 import java.nio.file.ProviderNotFoundException;
  42 import java.util.Collections;
  43 import java.util.HashMap;
  44 import java.util.Iterator;
  45 import java.util.List;
  46 import java.util.Map;
  47 import java.util.Properties;
  48 import java.util.regex.Pattern;
  49 import java.util.stream.Collectors;
  50 import java.util.stream.Stream;
  51 
  52 import jdk.internal.jimage.decompressor.CompressedResourceHeader;
  53 import jdk.internal.jimage.decompressor.ResourceDecompressor;
  54 import jdk.internal.jimage.decompressor.ResourceDecompressorFactory;
  55 import jdk.internal.jimage.decompressor.StringSharingDecompressorFactory;
  56 import jdk.internal.jimage.decompressor.ZipDecompressorFactory;
  57 import jdk.tools.jlink.internal.ResourcePoolManager;
  58 import jdk.tools.jlink.internal.StringTable;
  59 import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin;
  60 import jdk.tools.jlink.internal.plugins.StringSharingPlugin;
  61 import jdk.tools.jlink.internal.plugins.ZipPlugin;
  62 import jdk.tools.jlink.plugin.Plugin;
  63 import jdk.tools.jlink.plugin.ResourcePool;
  64 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
  65 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  66 
  67 public class CompressorPluginTest {
  68 
  69     private static int strID = 1;
  70 
  71     public static void main(String[] args) throws Exception {
  72         new CompressorPluginTest().test();
  73     }
  74 
  75     public void test() throws Exception {
  76         FileSystem fs;
  77         try {
  78             fs = FileSystems.getFileSystem(URI.create("jrt:/"));
  79         } catch (ProviderNotFoundException | FileSystemNotFoundException e) {
  80             System.err.println("Not an image build, test skipped.");
  81             return;
  82         }
  83         Path javabase = fs.getPath("/modules/java.base");
  84 
  85         checkCompress(gatherResources(javabase), new ZipPlugin(), null,
  86                 new ResourceDecompressorFactory[]{
  87                     new ZipDecompressorFactory()
  88                 });
  89 
  90         ResourcePool classes = gatherClasses(javabase);
  91         // compress = String sharing
  92         checkCompress(classes, new StringSharingPlugin(), null,
  93                 new ResourceDecompressorFactory[]{
  94                     new StringSharingDecompressorFactory()});
  95 
  96         // compress == ZIP + String sharing
  97         Properties options = new Properties();
  98         options.setProperty(ZipPlugin.NAME, "");
  99         checkCompress(classes, new DefaultCompressPlugin(), options,
 100                 new ResourceDecompressorFactory[]{
 101                     new ZipDecompressorFactory(),
 102                     new StringSharingDecompressorFactory()
 103                 });
 104 
 105         // compress == ZIP + String sharing + filter
 106         options.setProperty(DefaultCompressPlugin.FILTER,
 107                 "**Exception.class");
 108         checkCompress(classes, new DefaultCompressPlugin(), options,
 109                 new ResourceDecompressorFactory[]{
 110                     new ZipDecompressorFactory(),
 111                     new StringSharingDecompressorFactory()
 112                 }, Collections.singletonList(".*Exception.class"));
 113 
 114         // compress level 1 == ZIP
 115         Properties options1 = new Properties();
 116         options1.setProperty(DefaultCompressPlugin.NAME,
 117                 "1");
 118         checkCompress(classes, new DefaultCompressPlugin(),
 119                 options1,
 120                 new ResourceDecompressorFactory[]{
 121                     new ZipDecompressorFactory()
 122                 });
 123 
 124         // compress level 1 == ZIP
 125         options1.setProperty(DefaultCompressPlugin.FILTER,
 126                 "**Exception.class");
 127         checkCompress(classes, new DefaultCompressPlugin(),
 128                 options1,
 129                 new ResourceDecompressorFactory[]{
 130                     new ZipDecompressorFactory()
 131                 }, Collections.singletonList(".*Exception.class"));
 132 
 133         // compress level 2 == ZIP + String sharing
 134         Properties options2 = new Properties();
 135         options2.setProperty(DefaultCompressPlugin.NAME,
 136                 "2");
 137         checkCompress(classes, new DefaultCompressPlugin(),
 138                 options2,
 139                 new ResourceDecompressorFactory[]{
 140                     new ZipDecompressorFactory(),
 141                     new StringSharingDecompressorFactory()
 142                 });
 143 
 144         // compress level 2 == ZIP + String sharing + filter
 145         options2.setProperty(DefaultCompressPlugin.FILTER,
 146                 "**Exception.class");
 147         checkCompress(classes, new DefaultCompressPlugin(),
 148                 options2,
 149                 new ResourceDecompressorFactory[]{
 150                     new ZipDecompressorFactory(),
 151                     new StringSharingDecompressorFactory()
 152                 }, Collections.singletonList(".*Exception.class"));
 153 
 154         // compress level 0 == String sharing
 155         Properties options0 = new Properties();
 156         options0.setProperty(DefaultCompressPlugin.NAME, "0");
 157         checkCompress(classes, new DefaultCompressPlugin(),
 158                 options0,
 159                 new ResourceDecompressorFactory[]{
 160                     new StringSharingDecompressorFactory()
 161                 });
 162 
 163         // compress level 0 == String sharing + filter
 164         options0.setProperty(DefaultCompressPlugin.FILTER,
 165                 "**Exception.class");
 166         checkCompress(classes, new DefaultCompressPlugin(),
 167                 options0,
 168                 new ResourceDecompressorFactory[]{
 169                     new StringSharingDecompressorFactory()
 170                 }, Collections.singletonList(".*Exception.class"));
 171     }
 172 
 173     private ResourcePool gatherResources(Path module) throws Exception {
 174         ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 175 
 176             @Override
 177             public int addString(String str) {
 178                 return -1;
 179             }
 180 
 181             @Override
 182             public String getString(int id) {
 183                 return null;
 184             }
 185         });
 186 
 187         ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder();
 188         try (Stream<Path> stream = Files.walk(module)) {
 189             for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) {
 190                 Path p = iterator.next();
 191                 if (Files.isRegularFile(p)) {
 192                     byte[] content = Files.readAllBytes(p);
 193                     poolBuilder.add(ResourcePoolEntry.create(p.toString(), content));
 194                 }
 195             }
 196         }
 197         return poolBuilder.build();
 198     }
 199 
 200     private ResourcePool gatherClasses(Path module) throws Exception {
 201         ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 202 
 203             @Override
 204             public int addString(String str) {
 205                 return -1;
 206             }
 207 
 208             @Override
 209             public String getString(int id) {
 210                 return null;
 211             }
 212         });
 213 
 214         ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder();
 215         try (Stream<Path> stream = Files.walk(module)) {
 216             for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) {
 217                 Path p = iterator.next();
 218                 if (Files.isRegularFile(p) && p.toString().endsWith(".class")) {
 219                     byte[] content = Files.readAllBytes(p);
 220                     poolBuilder.add(ResourcePoolEntry.create(p.toString(), content));
 221                 }
 222             }
 223         }
 224         return poolBuilder.build();
 225     }
 226 
 227     private void checkCompress(ResourcePool resources, Plugin prov,
 228             Properties config,
 229             ResourceDecompressorFactory[] factories) throws Exception {
 230         checkCompress(resources, prov, config, factories, Collections.emptyList());
 231     }
 232 
 233     private void checkCompress(ResourcePool resources, Plugin prov,
 234             Properties config,
 235             ResourceDecompressorFactory[] factories,
 236             List<String> includes) throws Exception {
 237         long[] original = new long[1];
 238         long[] compressed = new long[1];
 239         resources.entries().forEach(resource -> {
 240             List<Pattern> includesPatterns = includes.stream()
 241                     .map(Pattern::compile)
 242                     .collect(Collectors.toList());
 243 
 244             Map<String, String> props = new HashMap<>();
 245             if (config != null) {
 246                 for (String p : config.stringPropertyNames()) {
 247                     props.put(p, config.getProperty(p));
 248                 }
 249             }
 250             prov.configure(props);
 251             final Map<Integer, String> strings = new HashMap<>();
 252             ResourcePoolManager inputResourcesMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 253                 @Override
 254                 public int addString(String str) {
 255                     int id = strID;
 256                     strID += 1;
 257                     strings.put(id, str);
 258                     return id;
 259                 }
 260 
 261                 @Override
 262                 public String getString(int id) {
 263                     return strings.get(id);
 264                 }
 265             });
 266             inputResourcesMgr.add(resource);
 267             ResourcePool compressedResources = applyCompressor(prov, inputResourcesMgr, resource, includesPatterns);
 268             original[0] += resource.contentLength();
 269             compressed[0] += compressedResources.findEntry(resource.path()).get().contentLength();
 270             applyDecompressors(factories, inputResourcesMgr.resourcePool(), compressedResources, strings, includesPatterns);
 271         });
 272         String compressors = Stream.of(factories)
 273                 .map(Object::getClass)
 274                 .map(Class::getSimpleName)
 275                 .collect(Collectors.joining(", "));
 276         String size = "Compressed size: " + compressed[0] + ", original size: " + original[0];
 277         System.out.println("Used " + compressors + ". " + size);
 278         if (original[0] <= compressed[0]) {
 279             throw new AssertionError("java.base not compressed.");
 280         }
 281     }
 282 
 283     private ResourcePool applyCompressor(Plugin plugin,
 284             ResourcePoolManager inputResources,
 285             ResourcePoolEntry res,
 286             List<Pattern> includesPatterns) {
 287         ResourcePoolManager resMgr = new ResourcePoolManager(ByteOrder.nativeOrder(),
 288                 inputResources.getStringTable());
 289         ResourcePool compressedResourcePool = plugin.transform(inputResources.resourcePool(),
 290             resMgr.resourcePoolBuilder());
 291         String path = res.path();
 292         ResourcePoolEntry compressed = compressedResourcePool.findEntry(path).get();
 293         CompressedResourceHeader header
 294                 = CompressedResourceHeader.readFromResource(ByteOrder.nativeOrder(), compressed.contentBytes());
 295         if (isIncluded(includesPatterns, path)) {
 296             if (header == null) {
 297                 throw new AssertionError("Path should be compressed: " + path);
 298             }
 299             if (header.getDecompressorNameOffset() == 0) {
 300                 throw new AssertionError("Invalid plugin offset "
 301                         + header.getDecompressorNameOffset());
 302             }
 303             if (header.getResourceSize() <= 0) {
 304                 throw new AssertionError("Invalid compressed size "
 305                         + header.getResourceSize());
 306             }
 307         } else if (header != null) {
 308             throw new AssertionError("Path should not be compressed: " + path);
 309         }
 310         return compressedResourcePool;
 311     }
 312 
 313     private void applyDecompressors(ResourceDecompressorFactory[] decompressors,
 314             ResourcePool inputResources,
 315             ResourcePool compressedResources,
 316             Map<Integer, String> strings,
 317             List<Pattern> includesPatterns) {
 318         compressedResources.entries().forEach(compressed -> {
 319             CompressedResourceHeader header = CompressedResourceHeader.readFromResource(
 320                     ByteOrder.nativeOrder(), compressed.contentBytes());
 321             String path = compressed.path();
 322             ResourcePoolEntry orig = inputResources.findEntry(path).get();
 323             if (!isIncluded(includesPatterns, path)) {
 324                 return;
 325             }
 326             byte[] decompressed = compressed.contentBytes();
 327             for (ResourceDecompressorFactory factory : decompressors) {
 328                 try {
 329                     ResourceDecompressor decompressor = factory.newDecompressor(new Properties());
 330                     decompressed = decompressor.decompress(
 331                         strings::get, decompressed,
 332                         CompressedResourceHeader.getSize(), header.getUncompressedSize());
 333                 } catch (Exception exp) {
 334                     throw new RuntimeException(exp);
 335                 }
 336             }
 337 
 338             if (decompressed.length != orig.contentLength()) {
 339                 throw new AssertionError("Invalid uncompressed size "
 340                         + header.getUncompressedSize());
 341             }
 342             byte[] origContent = orig.contentBytes();
 343             for (int i = 0; i < decompressed.length; i++) {
 344                 if (decompressed[i] != origContent[i]) {
 345                     throw new AssertionError("Decompressed and original differ at index " + i);
 346                 }
 347             }
 348         });
 349     }
 350 
 351     private boolean isIncluded(List<Pattern> includesPatterns, String path) {
 352         return includesPatterns.isEmpty() ||
 353                includesPatterns.stream().anyMatch((pattern) -> pattern.matcher(path).matches());
 354     }
 355 }