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