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 }