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  * @run main CompressorPluginTest
  32  */
  33 import java.net.URI;
  34 import java.nio.ByteOrder;
  35 import java.nio.file.FileSystem;
  36 import java.nio.file.FileSystemNotFoundException;
  37 import java.nio.file.FileSystems;
  38 import java.nio.file.Files;
  39 import java.nio.file.Path;
  40 import java.nio.file.ProviderNotFoundException;
  41 import java.util.Collections;
  42 import java.util.HashMap;
  43 import java.util.Iterator;
  44 import java.util.List;
  45 import java.util.Map;
  46 import java.util.Properties;
  47 import java.util.regex.Pattern;
  48 import java.util.stream.Collectors;
  49 import java.util.stream.Stream;
  50 
  51 import jdk.internal.jimage.decompressor.CompressedResourceHeader;
  52 import jdk.internal.jimage.decompressor.ResourceDecompressor;
  53 import jdk.internal.jimage.decompressor.ResourceDecompressorFactory;
  54 import jdk.internal.jimage.decompressor.StringSharingDecompressorFactory;
  55 import jdk.internal.jimage.decompressor.ZipDecompressorFactory;
  56 import jdk.tools.jlink.internal.ResourcePoolManager;
  57 import jdk.tools.jlink.internal.StringTable;
  58 import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin;
  59 import jdk.tools.jlink.internal.plugins.StringSharingPlugin;
  60 import jdk.tools.jlink.internal.plugins.ZipPlugin;
  61 import jdk.tools.jlink.plugin.Plugin;
  62 import jdk.tools.jlink.plugin.ResourcePool;
  63 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
  64 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  65 
  66 public class CompressorPluginTest {
  67 
  68     private static int strID = 1;
  69 
  70     public static void main(String[] args) throws Exception {
  71         new CompressorPluginTest().test();
  72     }
  73 
  74     public void test() throws Exception {
  75         FileSystem fs;
  76         try {
  77             fs = FileSystems.getFileSystem(URI.create("jrt:/"));
  78         } catch (ProviderNotFoundException | FileSystemNotFoundException e) {
  79             System.err.println("Not an image build, test skipped.");
  80             return;
  81         }
  82         Path javabase = fs.getPath("/modules/java.base");
  83 
  84         checkCompress(gatherResources(javabase), new ZipPlugin(), null,
  85                 new ResourceDecompressorFactory[]{
  86                     new ZipDecompressorFactory()
  87                 });
  88 
  89         ResourcePool classes = gatherClasses(javabase);
  90         // compress = String sharing
  91         checkCompress(classes, new StringSharingPlugin(), null,
  92                 new ResourceDecompressorFactory[]{
  93                     new StringSharingDecompressorFactory()});
  94 
  95         // compress == ZIP + String sharing
  96         Properties options = new Properties();
  97         options.setProperty(ZipPlugin.NAME, "");
  98         checkCompress(classes, new DefaultCompressPlugin(), options,
  99                 new ResourceDecompressorFactory[]{
 100                     new ZipDecompressorFactory(),
 101                     new StringSharingDecompressorFactory()
 102                 });
 103 
 104         // compress == ZIP + String sharing + filter
 105         options.setProperty(DefaultCompressPlugin.FILTER,
 106                 "**Exception.class");
 107         checkCompress(classes, new DefaultCompressPlugin(), options,
 108                 new ResourceDecompressorFactory[]{
 109                     new ZipDecompressorFactory(),
 110                     new StringSharingDecompressorFactory()
 111                 }, Collections.singletonList(".*Exception.class"));
 112 
 113         // compress level 1 == ZIP
 114         Properties options1 = new Properties();
 115         options1.setProperty(DefaultCompressPlugin.NAME,
 116                 "1");
 117         checkCompress(classes, new DefaultCompressPlugin(),
 118                 options1,
 119                 new ResourceDecompressorFactory[]{
 120                     new ZipDecompressorFactory()
 121                 });
 122 
 123         // compress level 1 == ZIP
 124         options1.setProperty(DefaultCompressPlugin.FILTER,
 125                 "**Exception.class");
 126         checkCompress(classes, new DefaultCompressPlugin(),
 127                 options1,
 128                 new ResourceDecompressorFactory[]{
 129                     new ZipDecompressorFactory()
 130                 }, Collections.singletonList(".*Exception.class"));
 131 
 132         // compress level 2 == ZIP + String sharing
 133         Properties options2 = new Properties();
 134         options2.setProperty(DefaultCompressPlugin.NAME,
 135                 "2");
 136         checkCompress(classes, new DefaultCompressPlugin(),
 137                 options2,
 138                 new ResourceDecompressorFactory[]{
 139                     new ZipDecompressorFactory(),
 140                     new StringSharingDecompressorFactory()
 141                 });
 142 
 143         // compress level 2 == ZIP + String sharing + filter
 144         options2.setProperty(DefaultCompressPlugin.FILTER,
 145                 "**Exception.class");
 146         checkCompress(classes, new DefaultCompressPlugin(),
 147                 options2,
 148                 new ResourceDecompressorFactory[]{
 149                     new ZipDecompressorFactory(),
 150                     new StringSharingDecompressorFactory()
 151                 }, Collections.singletonList(".*Exception.class"));
 152 
 153         // compress level 0 == String sharing
 154         Properties options0 = new Properties();
 155         options0.setProperty(DefaultCompressPlugin.NAME, "0");
 156         checkCompress(classes, new DefaultCompressPlugin(),
 157                 options0,
 158                 new ResourceDecompressorFactory[]{
 159                     new StringSharingDecompressorFactory()
 160                 });
 161 
 162         // compress level 0 == String sharing + filter
 163         options0.setProperty(DefaultCompressPlugin.FILTER,
 164                 "**Exception.class");
 165         checkCompress(classes, new DefaultCompressPlugin(),
 166                 options0,
 167                 new ResourceDecompressorFactory[]{
 168                     new StringSharingDecompressorFactory()
 169                 }, Collections.singletonList(".*Exception.class"));
 170     }
 171 
 172     private ResourcePool gatherResources(Path module) throws Exception {
 173         ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 174 
 175             @Override
 176             public int addString(String str) {
 177                 return -1;
 178             }
 179 
 180             @Override
 181             public String getString(int id) {
 182                 return null;
 183             }
 184         });
 185 
 186         ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder();
 187         try (Stream<Path> stream = Files.walk(module)) {
 188             for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) {
 189                 Path p = iterator.next();
 190                 if (Files.isRegularFile(p)) {
 191                     byte[] content = Files.readAllBytes(p);
 192                     poolBuilder.add(ResourcePoolEntry.create(p.toString(), content));
 193                 }
 194             }
 195         }
 196         return poolBuilder.build();
 197     }
 198 
 199     private ResourcePool gatherClasses(Path module) throws Exception {
 200         ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 201 
 202             @Override
 203             public int addString(String str) {
 204                 return -1;
 205             }
 206 
 207             @Override
 208             public String getString(int id) {
 209                 return null;
 210             }
 211         });
 212 
 213         ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder();
 214         try (Stream<Path> stream = Files.walk(module)) {
 215             for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) {
 216                 Path p = iterator.next();
 217                 if (Files.isRegularFile(p) && p.toString().endsWith(".class")) {
 218                     byte[] content = Files.readAllBytes(p);
 219                     poolBuilder.add(ResourcePoolEntry.create(p.toString(), content));
 220                 }
 221             }
 222         }
 223         return poolBuilder.build();
 224     }
 225 
 226     private void checkCompress(ResourcePool resources, Plugin prov,
 227             Properties config,
 228             ResourceDecompressorFactory[] factories) throws Exception {
 229         checkCompress(resources, prov, config, factories, Collections.emptyList());
 230     }
 231 
 232     private void checkCompress(ResourcePool resources, Plugin prov,
 233             Properties config,
 234             ResourceDecompressorFactory[] factories,
 235             List<String> includes) throws Exception {
 236         long[] original = new long[1];
 237         long[] compressed = new long[1];
 238         resources.entries().forEach(resource -> {
 239             List<Pattern> includesPatterns = includes.stream()
 240                     .map(Pattern::compile)
 241                     .collect(Collectors.toList());
 242 
 243             Map<String, String> props = new HashMap<>();
 244             if (config != null) {
 245                 for (String p : config.stringPropertyNames()) {
 246                     props.put(p, config.getProperty(p));
 247                 }
 248             }
 249             prov.configure(props);
 250             final Map<Integer, String> strings = new HashMap<>();
 251             ResourcePoolManager inputResourcesMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() {
 252                 @Override
 253                 public int addString(String str) {
 254                     int id = strID;
 255                     strID += 1;
 256                     strings.put(id, str);
 257                     return id;
 258                 }
 259 
 260                 @Override
 261                 public String getString(int id) {
 262                     return strings.get(id);
 263                 }
 264             });
 265             inputResourcesMgr.add(resource);
 266             ResourcePool compressedResources = applyCompressor(prov, inputResourcesMgr, resource, includesPatterns);
 267             original[0] += resource.contentLength();
 268             compressed[0] += compressedResources.findEntry(resource.path()).get().contentLength();
 269             applyDecompressors(factories, inputResourcesMgr.resourcePool(), compressedResources, strings, includesPatterns);
 270         });
 271         String compressors = Stream.of(factories)
 272                 .map(Object::getClass)
 273                 .map(Class::getSimpleName)
 274                 .collect(Collectors.joining(", "));
 275         String size = "Compressed size: " + compressed[0] + ", original size: " + original[0];
 276         System.out.println("Used " + compressors + ". " + size);
 277         if (original[0] <= compressed[0]) {
 278             throw new AssertionError("java.base not compressed.");
 279         }
 280     }
 281 
 282     private ResourcePool applyCompressor(Plugin plugin,
 283             ResourcePoolManager inputResources,
 284             ResourcePoolEntry res,
 285             List<Pattern> includesPatterns) {
 286         ResourcePoolManager resMgr = new ResourcePoolManager(ByteOrder.nativeOrder(),
 287                 inputResources.getStringTable());
 288         ResourcePool compressedResourcePool = plugin.transform(inputResources.resourcePool(),
 289             resMgr.resourcePoolBuilder());
 290         String path = res.path();
 291         ResourcePoolEntry compressed = compressedResourcePool.findEntry(path).get();
 292         CompressedResourceHeader header
 293                 = CompressedResourceHeader.readFromResource(ByteOrder.nativeOrder(), compressed.contentBytes());
 294         if (isIncluded(includesPatterns, path)) {
 295             if (header == null) {
 296                 throw new AssertionError("Path should be compressed: " + path);
 297             }
 298             if (header.getDecompressorNameOffset() == 0) {
 299                 throw new AssertionError("Invalid plugin offset "
 300                         + header.getDecompressorNameOffset());
 301             }
 302             if (header.getResourceSize() <= 0) {
 303                 throw new AssertionError("Invalid compressed size "
 304                         + header.getResourceSize());
 305             }
 306         } else if (header != null) {
 307             throw new AssertionError("Path should not be compressed: " + path);
 308         }
 309         return compressedResourcePool;
 310     }
 311 
 312     private void applyDecompressors(ResourceDecompressorFactory[] decompressors,
 313             ResourcePool inputResources,
 314             ResourcePool compressedResources,
 315             Map<Integer, String> strings,
 316             List<Pattern> includesPatterns) {
 317         compressedResources.entries().forEach(compressed -> {
 318             CompressedResourceHeader header = CompressedResourceHeader.readFromResource(
 319                     ByteOrder.nativeOrder(), compressed.contentBytes());
 320             String path = compressed.path();
 321             ResourcePoolEntry orig = inputResources.findEntry(path).get();
 322             if (!isIncluded(includesPatterns, path)) {
 323                 return;
 324             }
 325             byte[] decompressed = compressed.contentBytes();
 326             for (ResourceDecompressorFactory factory : decompressors) {
 327                 try {
 328                     ResourceDecompressor decompressor = factory.newDecompressor(new Properties());
 329                     decompressed = decompressor.decompress(
 330                         strings::get, decompressed,
 331                         CompressedResourceHeader.getSize(), header.getUncompressedSize());
 332                 } catch (Exception exp) {
 333                     throw new RuntimeException(exp);
 334                 }
 335             }
 336 
 337             if (decompressed.length != orig.contentLength()) {
 338                 throw new AssertionError("Invalid uncompressed size "
 339                         + header.getUncompressedSize());
 340             }
 341             byte[] origContent = orig.contentBytes();
 342             for (int i = 0; i < decompressed.length; i++) {
 343                 if (decompressed[i] != origContent[i]) {
 344                     throw new AssertionError("Decompressed and original differ at index " + i);
 345                 }
 346             }
 347         });
 348     }
 349 
 350     private boolean isIncluded(List<Pattern> includesPatterns, String path) {
 351         return includesPatterns.isEmpty() ||
 352                includesPatterns.stream().anyMatch((pattern) -> pattern.matcher(path).matches());
 353     }
 354 }