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 }