| /* | |
| * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @summary Test zip compressor | |
| * @author Jean-Francois Denise | |
| * @modules java.base/jdk.internal.jimage.decompressor | |
| * jdk.jlink/jdk.tools.jlink.internal | |
| * jdk.jlink/jdk.tools.jlink.internal.plugins | |
| * jdk.jlink/jdk.tools.jlink.plugin | |
| * @run main CompressorPluginTest | |
| */ | |
| import java.net.URI; | |
| import java.nio.ByteOrder; | |
| import java.nio.file.FileSystem; | |
| import java.nio.file.FileSystemNotFoundException; | |
| import java.nio.file.FileSystems; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.ProviderNotFoundException; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.Iterator; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Properties; | |
| import java.util.regex.Pattern; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import jdk.internal.jimage.decompressor.CompressedResourceHeader; | |
| import jdk.internal.jimage.decompressor.ResourceDecompressor; | |
| import jdk.internal.jimage.decompressor.ResourceDecompressorFactory; | |
| import jdk.internal.jimage.decompressor.StringSharingDecompressorFactory; | |
| import jdk.internal.jimage.decompressor.ZipDecompressorFactory; | |
| import jdk.tools.jlink.internal.ResourcePoolManager; | |
| import jdk.tools.jlink.internal.StringTable; | |
| import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; | |
| import jdk.tools.jlink.internal.plugins.StringSharingPlugin; | |
| import jdk.tools.jlink.internal.plugins.ZipPlugin; | |
| import jdk.tools.jlink.plugin.Plugin; | |
| import jdk.tools.jlink.plugin.ResourcePool; | |
| import jdk.tools.jlink.plugin.ResourcePoolBuilder; | |
| import jdk.tools.jlink.plugin.ResourcePoolEntry; | |
| public class CompressorPluginTest { | |
| private static int strID = 1; | |
| public static void main(String[] args) throws Exception { | |
| new CompressorPluginTest().test(); | |
| } | |
| public void test() throws Exception { | |
| FileSystem fs; | |
| try { | |
| fs = FileSystems.getFileSystem(URI.create("jrt:/")); | |
| } catch (ProviderNotFoundException | FileSystemNotFoundException e) { | |
| System.err.println("Not an image build, test skipped."); | |
| return; | |
| } | |
| Path javabase = fs.getPath("/modules/java.base"); | |
| checkCompress(gatherResources(javabase), new ZipPlugin(), null, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory() | |
| }); | |
| ResourcePool classes = gatherClasses(javabase); | |
| // compress = String sharing | |
| checkCompress(classes, new StringSharingPlugin(), null, | |
| new ResourceDecompressorFactory[]{ | |
| new StringSharingDecompressorFactory()}); | |
| // compress level 0 == no compression | |
| Properties options0 = new Properties(); | |
| DefaultCompressPlugin compressPlugin = new DefaultCompressPlugin(); | |
| options0.setProperty(compressPlugin.getName(), | |
| DefaultCompressPlugin.LEVEL_0); | |
| checkCompress(classes, compressPlugin, | |
| options0, | |
| new ResourceDecompressorFactory[]{ | |
| }); | |
| // compress level 1 == String sharing | |
| Properties options1 = new Properties(); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| options1.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_1); | |
| checkCompress(classes, compressPlugin, | |
| options1, | |
| new ResourceDecompressorFactory[]{ | |
| new StringSharingDecompressorFactory() | |
| }); | |
| // compress level 1 == String sharing + filter | |
| options1.setProperty(DefaultCompressPlugin.FILTER, | |
| "**Exception.class"); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| options1.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_1); | |
| checkCompress(classes, compressPlugin, | |
| options1, | |
| new ResourceDecompressorFactory[]{ | |
| new StringSharingDecompressorFactory() | |
| }, Collections.singletonList(".*Exception.class")); | |
| // compress level 2 == ZIP | |
| Properties options2 = new Properties(); | |
| options2.setProperty(DefaultCompressPlugin.FILTER, | |
| "**Exception.class"); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| options2.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_2); | |
| checkCompress(classes, compressPlugin, | |
| options2, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory() | |
| }, Collections.singletonList(".*Exception.class")); | |
| // compress level 2 == ZIP + filter | |
| options2.setProperty(DefaultCompressPlugin.FILTER, | |
| "**Exception.class"); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| options2.setProperty(compressPlugin.getName(), DefaultCompressPlugin.LEVEL_2); | |
| checkCompress(classes, compressPlugin, | |
| options2, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory(), | |
| }, Collections.singletonList(".*Exception.class")); | |
| // compress level zip-0 == no compression | |
| Properties optionsZip0 = new Properties(); | |
| DefaultCompressPlugin compressPluginZip0 = new DefaultCompressPlugin(); | |
| optionsZip0.setProperty(compressPluginZip0.getName(), "zip-0"); | |
| checkCompress(classes, compressPluginZip0, | |
| optionsZip0, | |
| new ResourceDecompressorFactory[]{ | |
| }); | |
| // compress level zip-[1-9] == varied compression levels | |
| for(int i = 1; i < 10; i++) { | |
| Properties optionsZip = new Properties(); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| optionsZip.setProperty(compressPlugin.getName(), "zip-" + i); | |
| checkCompress(classes, compressPlugin, | |
| optionsZip, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory(), | |
| }); | |
| } | |
| // compress level zip-[1-9] == varied compression levels + filter | |
| for(int i = 1; i < 10; i++) { | |
| Properties optionsZip = new Properties(); | |
| compressPlugin = new DefaultCompressPlugin(); | |
| optionsZip.setProperty(DefaultCompressPlugin.FILTER, "**Exception.class"); | |
| optionsZip.setProperty(compressPlugin.getName(), "zip-" + i); | |
| checkCompress(classes, compressPlugin, | |
| optionsZip, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory(), | |
| }, Collections.singletonList(".*Exception.class")); | |
| } | |
| testBadCompressProps(classes, "zip-10"); | |
| testBadCompressProps(classes, "zip-badarg"); | |
| testBadCompressProps(classes, "zip-10000000"); | |
| } | |
| private void testBadCompressProps(ResourcePool classes, String compressArg) throws Exception { | |
| Properties badProps = new Properties(); | |
| DefaultCompressPlugin compressPlugin = new DefaultCompressPlugin(); | |
| badProps.setProperty(compressPlugin.getName(), compressArg); | |
| try { | |
| checkCompress(classes, compressPlugin, | |
| badProps, | |
| new ResourceDecompressorFactory[]{ | |
| new ZipDecompressorFactory(), | |
| }); | |
| } catch (IllegalArgumentException e) { | |
| if (e.getMessage().contains("Invalid compression level")) { | |
| return; | |
| } else { | |
| throw e; | |
| } | |
| } | |
| throw new Exception("Expected compression IAE with " + compressArg + " but didn't get one."); | |
| } | |
| private ResourcePool gatherResources(Path module) throws Exception { | |
| ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { | |
| @Override | |
| public int addString(String str) { | |
| return -1; | |
| } | |
| @Override | |
| public String getString(int id) { | |
| return null; | |
| } | |
| }); | |
| ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder(); | |
| try (Stream<Path> stream = Files.walk(module)) { | |
| for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) { | |
| Path p = iterator.next(); | |
| if (Files.isRegularFile(p)) { | |
| byte[] content = Files.readAllBytes(p); | |
| poolBuilder.add(ResourcePoolEntry.create(p.toString(), content)); | |
| } | |
| } | |
| } | |
| return poolBuilder.build(); | |
| } | |
| private ResourcePool gatherClasses(Path module) throws Exception { | |
| ResourcePoolManager poolMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { | |
| @Override | |
| public int addString(String str) { | |
| return -1; | |
| } | |
| @Override | |
| public String getString(int id) { | |
| return null; | |
| } | |
| }); | |
| ResourcePoolBuilder poolBuilder = poolMgr.resourcePoolBuilder(); | |
| try (Stream<Path> stream = Files.walk(module)) { | |
| for (Iterator<Path> iterator = stream.iterator(); iterator.hasNext();) { | |
| Path p = iterator.next(); | |
| if (Files.isRegularFile(p) && p.toString().endsWith(".class")) { | |
| byte[] content = Files.readAllBytes(p); | |
| poolBuilder.add(ResourcePoolEntry.create(p.toString(), content)); | |
| } | |
| } | |
| } | |
| return poolBuilder.build(); | |
| } | |
| private void checkCompress(ResourcePool resources, Plugin prov, | |
| Properties config, | |
| ResourceDecompressorFactory[] factories) throws Exception { | |
| checkCompress(resources, prov, config, factories, Collections.emptyList()); | |
| } | |
| private void checkCompress(ResourcePool resources, Plugin prov, | |
| Properties config, | |
| ResourceDecompressorFactory[] factories, | |
| List<String> includes) throws Exception { | |
| if (factories.length == 0) { | |
| // no compression, nothing to check! | |
| return; | |
| } | |
| long[] original = new long[1]; | |
| long[] compressed = new long[1]; | |
| resources.entries().forEach(resource -> { | |
| List<Pattern> includesPatterns = includes.stream() | |
| .map(Pattern::compile) | |
| .collect(Collectors.toList()); | |
| Map<String, String> props = new HashMap<>(); | |
| if (config != null) { | |
| for (String p : config.stringPropertyNames()) { | |
| props.put(p, config.getProperty(p)); | |
| } | |
| } | |
| prov.configure(props); | |
| final Map<Integer, String> strings = new HashMap<>(); | |
| ResourcePoolManager inputResourcesMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), new StringTable() { | |
| @Override | |
| public int addString(String str) { | |
| int id = strID; | |
| strID += 1; | |
| strings.put(id, str); | |
| return id; | |
| } | |
| @Override | |
| public String getString(int id) { | |
| return strings.get(id); | |
| } | |
| }); | |
| inputResourcesMgr.add(resource); | |
| ResourcePool compressedResources = applyCompressor(prov, inputResourcesMgr, resource, includesPatterns); | |
| original[0] += resource.contentLength(); | |
| compressed[0] += compressedResources.findEntry(resource.path()).get().contentLength(); | |
| applyDecompressors(factories, inputResourcesMgr.resourcePool(), compressedResources, strings, includesPatterns); | |
| }); | |
| String compressors = Stream.of(factories) | |
| .map(Object::getClass) | |
| .map(Class::getSimpleName) | |
| .collect(Collectors.joining(", ")); | |
| String size = "Compressed size: " + compressed[0] + ", original size: " + original[0]; | |
| System.out.println("Used " + compressors + ". " + size); | |
| if (original[0] <= compressed[0]) { | |
| throw new AssertionError("java.base not compressed."); | |
| } | |
| } | |
| private ResourcePool applyCompressor(Plugin plugin, | |
| ResourcePoolManager inputResources, | |
| ResourcePoolEntry res, | |
| List<Pattern> includesPatterns) { | |
| ResourcePoolManager resMgr = new ResourcePoolManager(ByteOrder.nativeOrder(), | |
| inputResources.getStringTable()); | |
| ResourcePool compressedResourcePool = plugin.transform(inputResources.resourcePool(), | |
| resMgr.resourcePoolBuilder()); | |
| String path = res.path(); | |
| ResourcePoolEntry compressed = compressedResourcePool.findEntry(path).get(); | |
| CompressedResourceHeader header | |
| = CompressedResourceHeader.readFromResource(ByteOrder.nativeOrder(), compressed.contentBytes()); | |
| if (isIncluded(includesPatterns, path)) { | |
| if (header == null) { | |
| throw new AssertionError("Path should be compressed: " + path); | |
| } | |
| if (header.getDecompressorNameOffset() == 0) { | |
| throw new AssertionError("Invalid plugin offset " | |
| + header.getDecompressorNameOffset()); | |
| } | |
| if (header.getResourceSize() <= 0) { | |
| throw new AssertionError("Invalid compressed size " | |
| + header.getResourceSize()); | |
| } | |
| } else if (header != null) { | |
| throw new AssertionError("Path should not be compressed: " + path); | |
| } | |
| return compressedResourcePool; | |
| } | |
| private void applyDecompressors(ResourceDecompressorFactory[] decompressors, | |
| ResourcePool inputResources, | |
| ResourcePool compressedResources, | |
| Map<Integer, String> strings, | |
| List<Pattern> includesPatterns) { | |
| compressedResources.entries().forEach(compressed -> { | |
| CompressedResourceHeader header = CompressedResourceHeader.readFromResource( | |
| ByteOrder.nativeOrder(), compressed.contentBytes()); | |
| String path = compressed.path(); | |
| ResourcePoolEntry orig = inputResources.findEntry(path).get(); | |
| if (!isIncluded(includesPatterns, path)) { | |
| return; | |
| } | |
| byte[] decompressed = compressed.contentBytes(); | |
| for (ResourceDecompressorFactory factory : decompressors) { | |
| try { | |
| ResourceDecompressor decompressor = factory.newDecompressor(); | |
| decompressed = decompressor.decompress( | |
| strings::get, decompressed, | |
| CompressedResourceHeader.getSize(), header.getUncompressedSize()); | |
| } catch (Exception exp) { | |
| throw new RuntimeException(exp); | |
| } | |
| } | |
| if (decompressed.length != orig.contentLength()) { | |
| throw new AssertionError("Invalid uncompressed size " | |
| + header.getUncompressedSize()); | |
| } | |
| byte[] origContent = orig.contentBytes(); | |
| for (int i = 0; i < decompressed.length; i++) { | |
| if (decompressed[i] != origContent[i]) { | |
| throw new AssertionError("Decompressed and original differ at index " + i); | |
| } | |
| } | |
| }); | |
| } | |
| private boolean isIncluded(List<Pattern> includesPatterns, String path) { | |
| return includesPatterns.isEmpty() || | |
| includesPatterns.stream().anyMatch((pattern) -> pattern.matcher(path).matches()); | |
| } | |
| } |