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