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