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 main(String[] args)71 public static void main(String[] args) throws Exception { 72 new CompressorPluginTest().test(); 73 } 74 test()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 level 0 == no compression 97 Properties options0 = new Properties(); 98 DefaultCompressPlugin compressPlugin = new DefaultCompressPlugin(); 99 options0.setProperty(compressPlugin.getName(), 100 DefaultCompressPlugin.LEVEL_0); 101 checkCompress(classes, compressPlugin, 102 options0, 103 new ResourceDecompressorFactory[]{ 104 }); 105 106 // compress level 1 == String sharing 107 Properties options1 = new Properties(); 108 compressPlugin = new DefaultCompressPlugin(); 109 options1.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_1); 110 checkCompress(classes, compressPlugin, 111 options1, 112 new ResourceDecompressorFactory[]{ 113 new StringSharingDecompressorFactory() 114 }); 115 116 // compress level 1 == String sharing + filter 117 options1.setProperty(DefaultCompressPlugin.FILTER, 118 "**Exception.class"); 119 compressPlugin = new DefaultCompressPlugin(); 120 options1.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_1); 121 checkCompress(classes, compressPlugin, 122 options1, 123 new ResourceDecompressorFactory[]{ 124 new StringSharingDecompressorFactory() 125 }, Collections.singletonList(".*Exception.class")); 126 127 // compress level 2 == ZIP 128 Properties options2 = new Properties(); 129 options2.setProperty(DefaultCompressPlugin.FILTER, 130 "**Exception.class"); 131 compressPlugin = new DefaultCompressPlugin(); 132 options2.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_2); 133 checkCompress(classes, compressPlugin, 134 options2, 135 new ResourceDecompressorFactory[]{ 136 new ZipDecompressorFactory() 137 }, Collections.singletonList(".*Exception.class")); 138 139 // compress level 2 == ZIP + filter 140 options2.setProperty(DefaultCompressPlugin.FILTER, 141 "**Exception.class"); 142 compressPlugin = new DefaultCompressPlugin(); 143 options2.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_2); 144 checkCompress(classes, compressPlugin, 145 options2, 146 new ResourceDecompressorFactory[]{ 147 new ZipDecompressorFactory(), 148 }, Collections.singletonList(".*Exception.class")); 149 } 150 gatherResources(Path module)151 private ResourcePool gatherResources(Path module) throws Exception { 152 ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { 153 154 @Override 155 public int addString(String str) { 156 return -1; 157 } 158 159 @Override 160 public String getString(int id) { 161 return null; 162 } 163 }); 164 165 ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder(); 166 try (Stream<Path> stream = Files.walk(module)) { 167 for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) { 168 Path p = iterator.next(); 169 if (Files.isRegularFile(p)) { 170 byte[] content = Files.readAllBytes(p); 171 poolBuilder.add(ResourcePoolEntry.create(p.toString(), content)); 172 } 173 } 174 } 175 return poolBuilder.build(); 176 } 177 gatherClasses(Path module)178 private ResourcePool gatherClasses(Path module) throws Exception { 179 ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { 180 181 @Override 182 public int addString(String str) { 183 return -1; 184 } 185 186 @Override 187 public String getString(int id) { 188 return null; 189 } 190 }); 191 192 ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder(); 193 try (Stream<Path> stream = Files.walk(module)) { 194 for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) { 195 Path p = iterator.next(); 196 if (Files.isRegularFile(p) && p.toString().endsWith(".class")) { 197 byte[] content = Files.readAllBytes(p); 198 poolBuilder.add(ResourcePoolEntry.create(p.toString(), content)); 199 } 200 } 201 } 202 return poolBuilder.build(); 203 } 204 checkCompress(ResourcePool resources, Plugin prov, Properties config, ResourceDecompressorFactory[] factories)205 private void checkCompress(ResourcePool resources, Plugin prov, 206 Properties config, 207 ResourceDecompressorFactory[] factories) throws Exception { 208 checkCompress(resources, prov, config, factories, Collections.emptyList()); 209 } 210 checkCompress(ResourcePool resources, Plugin prov, Properties config, ResourceDecompressorFactory[] factories, List<String> includes)211 private void checkCompress(ResourcePool resources, Plugin prov, 212 Properties config, 213 ResourceDecompressorFactory[] factories, 214 List<String> includes) throws Exception { 215 if (factories.length == 0) { 216 // no compression, nothing to check! 217 return; 218 } 219 220 long[] original = new long[1]; 221 long[] compressed = new long[1]; 222 resources.entries().forEach(resource -> { 223 List<Pattern> includesPatterns = includes.stream() 224 .map(Pattern::compile) 225 .collect(Collectors.toList()); 226 227 Map<String, String> props = new HashMap<>(); 228 if (config != null) { 229 for (String p : config.stringPropertyNames()) { 230 props.put(p, config.getProperty(p)); 231 } 232 } 233 prov.configure(props); 234 final Map<Integer, String> strings = new HashMap<>(); 235 ResourcePoolManager inputResourcesMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { 236 @Override 237 public int addString(String str) { 238 int id = strID; 239 strID += 1; 240 strings.put(id, str); 241 return id; 242 } 243 244 @Override 245 public String getString(int id) { 246 return strings.get(id); 247 } 248 }); 249 inputResourcesMgr.add(resource); 250 ResourcePool compressedResources = applyCompressor(prov, inputResourcesMgr, resource, includesPatterns); 251 original[0] += resource.contentLength(); 252 compressed[0] += compressedResources.findEntry(resource.path()).get().contentLength(); 253 applyDecompressors(factories, inputResourcesMgr.resourcePool(), compressedResources, strings, includesPatterns); 254 }); 255 String compressors = Stream.of(factories) 256 .map(Object::getClass) 257 .map(Class::getSimpleName) 258 .collect(Collectors.joining(", ")); 259 String size = "Compressed size: " + compressed[0] + ", original size: " + original[0]; 260 System.out.println("Used " + compressors + ". " + size); 261 if (original[0] <= compressed[0]) { 262 throw new AssertionError("java.base not compressed."); 263 } 264 } 265 applyCompressor(Plugin plugin, ResourcePoolManager inputResources, ResourcePoolEntry res, List<Pattern> includesPatterns)266 private ResourcePool applyCompressor(Plugin plugin, 267 ResourcePoolManager inputResources, 268 ResourcePoolEntry res, 269 List<Pattern> includesPatterns) { 270 ResourcePoolManager resMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), 271 inputResources.getStringTable()); 272 ResourcePool compressedResourcePool = plugin.transform(inputResources.resourcePool(), 273 resMgr.resourcePoolBuilder()); 274 String path = res.path(); 275 ResourcePoolEntry compressed = compressedResourcePool.findEntry(path).get(); 276 CompressedResourceHeader header 277 = CompressedResourceHeader.readFromResource(ByteOrder.nativeOrder(), compressed.contentBytes()); 278 if (isIncluded(includesPatterns, path)) { 279 if (header == null) { 280 throw new AssertionError("Path should be compressed: " + path); 281 } 282 if (header.getDecompressorNameOffset() == 0) { 283 throw new AssertionError("Invalid plugin offset " 284 + header.getDecompressorNameOffset()); 285 } 286 if (header.getResourceSize() <= 0) { 287 throw new AssertionError("Invalid compressed size " 288 + header.getResourceSize()); 289 } 290 } else if (header != null) { 291 throw new AssertionError("Path should not be compressed: " + path); 292 } 293 return compressedResourcePool; 294 } 295 applyDecompressors(ResourceDecompressorFactory[] decompressors, ResourcePool inputResources, ResourcePool compressedResources, Map<Integer, String> strings, List<Pattern> includesPatterns)296 private void applyDecompressors(ResourceDecompressorFactory[] decompressors, 297 ResourcePool inputResources, 298 ResourcePool compressedResources, 299 Map<Integer, String> strings, 300 List<Pattern> includesPatterns) { 301 compressedResources.entries().forEach(compressed -> { 302 CompressedResourceHeader header = CompressedResourceHeader.readFromResource( 303 ByteOrder.nativeOrder(), compressed.contentBytes()); 304 String path = compressed.path(); 305 ResourcePoolEntry orig = inputResources.findEntry(path).get(); 306 if (!isIncluded(includesPatterns, path)) { 307 return; 308 } 309 byte[] decompressed = compressed.contentBytes(); 310 for (ResourceDecompressorFactory factory : decompressors) { 311 try { 312 ResourceDecompressor decompressor = factory.newDecompressor(new Properties()); 313 decompressed = decompressor.decompress( 314 strings::get, decompressed, 315 CompressedResourceHeader.getSize(), header.getUncompressedSize()); 316 } catch (Exception exp) { 317 throw new RuntimeException(exp); 318 } 319 } 320 321 if (decompressed.length != orig.contentLength()) { 322 throw new AssertionError("Invalid uncompressed size " 323 + header.getUncompressedSize()); 324 } 325 byte[] origContent = orig.contentBytes(); 326 for (int i = 0; i < decompressed.length; i++) { 327 if (decompressed[i] != origContent[i]) { 328 throw new AssertionError("Decompressed and original differ at index " + i); 329 } 330 } 331 }); 332 } 333 isIncluded(List<Pattern> includesPatterns, String path)334 private boolean isIncluded(List<Pattern> includesPatterns, String path) { 335 return includesPatterns.isEmpty() || 336 includesPatterns.stream().anyMatch((pattern) -> pattern.matcher(path).matches()); 337 } 338 } 339