1 /*
2  * Copyright (c) 2019, 2020, 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 package jdk.jpackage.test;
24 
25 import java.io.IOException;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Optional;
35 import java.util.Set;
36 import java.util.function.Function;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 import java.util.stream.Collectors;
40 import java.util.stream.Stream;
41 import jdk.jpackage.internal.IOUtils;
42 import jdk.jpackage.test.PackageTest.PackageHandlers;
43 
44 
45 
46 public class LinuxHelper {
getRelease(JPackageCommand cmd)47     private static String getRelease(JPackageCommand cmd) {
48         return cmd.getArgumentValue("--linux-app-release", () -> "1");
49     }
50 
getPackageName(JPackageCommand cmd)51     public static String getPackageName(JPackageCommand cmd) {
52         cmd.verifyIsOfType(PackageType.LINUX);
53         return cmd.getArgumentValue("--linux-package-name",
54                 () -> cmd.installerName().toLowerCase());
55     }
56 
getDesktopFile(JPackageCommand cmd)57     public static Path getDesktopFile(JPackageCommand cmd) {
58         return getDesktopFile(cmd, null);
59     }
60 
getDesktopFile(JPackageCommand cmd, String launcherName)61     public static Path getDesktopFile(JPackageCommand cmd, String launcherName) {
62         cmd.verifyIsOfType(PackageType.LINUX);
63         String desktopFileName = String.format("%s-%s.desktop", getPackageName(
64                 cmd), Optional.ofNullable(launcherName).orElseGet(
65                         () -> cmd.name()).replaceAll("\\s+", "_"));
66         return cmd.appLayout().destktopIntegrationDirectory().resolve(
67                 desktopFileName);
68     }
69 
getBundleName(JPackageCommand cmd)70     static String getBundleName(JPackageCommand cmd) {
71         cmd.verifyIsOfType(PackageType.LINUX);
72 
73         final PackageType packageType = cmd.packageType();
74         String format = null;
75         switch (packageType) {
76             case LINUX_DEB:
77                 format = "%s_%s-%s_%s";
78                 break;
79 
80             case LINUX_RPM:
81                 format = "%s-%s-%s.%s";
82                 break;
83         }
84 
85         final String release = getRelease(cmd);
86         final String version = cmd.version();
87 
88         return String.format(format, getPackageName(cmd), version, release,
89                 getDefaultPackageArch(packageType)) + packageType.getSuffix();
90     }
91 
getPackageFiles(JPackageCommand cmd)92     public static Stream<Path> getPackageFiles(JPackageCommand cmd) {
93         cmd.verifyIsOfType(PackageType.LINUX);
94 
95         final PackageType packageType = cmd.packageType();
96         final Path packageFile = cmd.outputBundle();
97 
98         Executor exec = null;
99         switch (packageType) {
100             case LINUX_DEB:
101                 exec = Executor.of("dpkg", "--contents").addArgument(packageFile);
102                 break;
103 
104             case LINUX_RPM:
105                 exec = Executor.of("rpm", "-qpl").addArgument(packageFile);
106                 break;
107         }
108 
109         Stream<String> lines = exec.executeAndGetOutput().stream();
110         if (packageType == PackageType.LINUX_DEB) {
111             // Typical text lines produced by dpkg look like:
112             // drwxr-xr-x root/root         0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/
113             // -rw-r--r-- root/root    574912 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/libmlib_image.so
114             // Need to skip all fields but absolute path to file.
115             lines = lines.map(line -> line.substring(line.indexOf(" ./") + 2));
116         }
117         return lines.map(Path::of);
118     }
119 
getPrerequisitePackages(JPackageCommand cmd)120     public static List<String> getPrerequisitePackages(JPackageCommand cmd) {
121         cmd.verifyIsOfType(PackageType.LINUX);
122         var packageType = cmd.packageType();
123         switch (packageType) {
124             case LINUX_DEB:
125                 return Stream.of(getDebBundleProperty(cmd.outputBundle(),
126                         "Depends").split(",")).map(String::strip).collect(
127                         Collectors.toList());
128 
129             case LINUX_RPM:
130                 return Executor.of("rpm", "-qp", "-R")
131                 .addArgument(cmd.outputBundle())
132                 .executeAndGetOutput();
133         }
134         // Unreachable
135         return null;
136     }
137 
getBundleProperty(JPackageCommand cmd, String propertyName)138     public static String getBundleProperty(JPackageCommand cmd,
139             String propertyName) {
140         return getBundleProperty(cmd,
141                 Map.of(PackageType.LINUX_DEB, propertyName,
142                         PackageType.LINUX_RPM, propertyName));
143     }
144 
getBundleProperty(JPackageCommand cmd, Map<PackageType, String> propertyName)145     public static String getBundleProperty(JPackageCommand cmd,
146             Map<PackageType, String> propertyName) {
147         cmd.verifyIsOfType(PackageType.LINUX);
148         var packageType = cmd.packageType();
149         switch (packageType) {
150             case LINUX_DEB:
151                 return getDebBundleProperty(cmd.outputBundle(), propertyName.get(
152                         packageType));
153 
154             case LINUX_RPM:
155                 return getRpmBundleProperty(cmd.outputBundle(), propertyName.get(
156                         packageType));
157         }
158         // Unrechable
159         return null;
160     }
161 
createDebPackageHandlers()162     static PackageHandlers createDebPackageHandlers() {
163         PackageHandlers deb = new PackageHandlers();
164         deb.installHandler = cmd -> {
165             cmd.verifyIsOfType(PackageType.LINUX_DEB);
166             Executor.of("sudo", "dpkg", "-i")
167             .addArgument(cmd.outputBundle())
168             .execute();
169         };
170         deb.uninstallHandler = cmd -> {
171             cmd.verifyIsOfType(PackageType.LINUX_DEB);
172             Executor.of("sudo", "dpkg", "-r", getPackageName(cmd)).execute();
173         };
174         deb.unpackHandler = (cmd, destinationDir) -> {
175             cmd.verifyIsOfType(PackageType.LINUX_DEB);
176             Executor.of("dpkg", "-x")
177             .addArgument(cmd.outputBundle())
178             .addArgument(destinationDir)
179             .execute();
180             return destinationDir;
181         };
182         return deb;
183     }
184 
createRpmPackageHandlers()185     static PackageHandlers createRpmPackageHandlers() {
186         PackageHandlers rpm = new PackageHandlers();
187         rpm.installHandler = cmd -> {
188             cmd.verifyIsOfType(PackageType.LINUX_RPM);
189             Executor.of("sudo", "rpm", "-i")
190             .addArgument(cmd.outputBundle())
191             .execute();
192         };
193         rpm.uninstallHandler = cmd -> {
194             cmd.verifyIsOfType(PackageType.LINUX_RPM);
195             Executor.of("sudo", "rpm", "-e", getPackageName(cmd)).execute();
196         };
197         rpm.unpackHandler = (cmd, destinationDir) -> {
198             cmd.verifyIsOfType(PackageType.LINUX_RPM);
199             Executor.of("sh", "-c", String.format(
200                     "rpm2cpio '%s' | cpio -idm --quiet",
201                     JPackageCommand.escapeAndJoin(
202                             cmd.outputBundle().toAbsolutePath().toString())))
203             .setDirectory(destinationDir)
204             .execute();
205             return destinationDir;
206         };
207 
208         return rpm;
209     }
210 
getLauncherPath(JPackageCommand cmd)211     static Path getLauncherPath(JPackageCommand cmd) {
212         cmd.verifyIsOfType(PackageType.LINUX);
213 
214         final String launcherName = cmd.name();
215         final String launcherRelativePath = Path.of("/bin", launcherName).toString();
216 
217         return getPackageFiles(cmd).filter(path -> path.toString().endsWith(
218                 launcherRelativePath)).findFirst().or(() -> {
219             TKit.assertUnexpected(String.format(
220                     "Failed to find %s in %s package", launcherName,
221                     getPackageName(cmd)));
222             return null;
223         }).get();
224     }
225 
getInstalledPackageSizeKB(JPackageCommand cmd)226     static long getInstalledPackageSizeKB(JPackageCommand cmd) {
227         cmd.verifyIsOfType(PackageType.LINUX);
228 
229         final Path packageFile = cmd.outputBundle();
230         switch (cmd.packageType()) {
231             case LINUX_DEB:
232                 Long estimate = Long.parseLong(getDebBundleProperty(packageFile,
233                         "Installed-Size"));
234                 if (estimate == 0L) {
235                     // if the estimate in KB is 0, check if it is really empty
236                     // or just < 1KB as with AppImagePackageTest.testEmpty()
237                     if (getPackageFiles(cmd).count() > 01L) {
238                         // there is something there so round up to 1 KB
239                         estimate = 01L;
240                     }
241                 }
242                 return estimate;
243 
244             case LINUX_RPM:
245                 String size = getRpmBundleProperty(packageFile, "Size");
246                 return (Long.parseLong(size) + 1023L) >> 10; // in KB rounded up
247 
248         }
249 
250         return 0;
251     }
252 
getDebBundleProperty(Path bundle, String fieldName)253     static String getDebBundleProperty(Path bundle, String fieldName) {
254         return Executor.of("dpkg-deb", "-f")
255                 .addArgument(bundle)
256                 .addArgument(fieldName)
257                 .executeAndGetFirstLineOfOutput();
258     }
259 
getRpmBundleProperty(Path bundle, String fieldName)260     static String getRpmBundleProperty(Path bundle, String fieldName) {
261         return Executor.of("rpm", "-qp", "--queryformat", String.format("%%{%s}", fieldName))
262                 .addArgument(bundle)
263                 .executeAndGetFirstLineOfOutput();
264     }
265 
verifyPackageBundleEssential(JPackageCommand cmd)266     static void verifyPackageBundleEssential(JPackageCommand cmd) {
267         String packageName = LinuxHelper.getPackageName(cmd);
268         Long packageSize = LinuxHelper.getInstalledPackageSizeKB(cmd);
269         TKit.trace("InstalledPackageSize: " + packageSize);
270         TKit.assertNotEquals(0L, packageSize, String.format(
271                 "Check installed size of [%s] package in not zero", packageName));
272 
273         final boolean checkPrerequisites;
274         if (cmd.isRuntime()) {
275             Path runtimeDir = cmd.appRuntimeDirectory();
276             Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map(
277                     runtimeDir::resolve).collect(Collectors.toSet());
278             Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter(
279                     expectedCriticalRuntimePaths::contains).collect(
280                             Collectors.toSet());
281             checkPrerequisites = expectedCriticalRuntimePaths.equals(
282                     actualCriticalRuntimePaths);
283         } else {
284             // AppImagePackageTest.testEmpty() will have no dependencies,
285             // but will have more then 0 and less than 1K content size.
286             checkPrerequisites = packageSize > 1;
287         }
288 
289         List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd);
290         if (checkPrerequisites) {
291             final String vitalPackage = "libc";
292             TKit.assertTrue(prerequisites.stream().filter(
293                     dep -> dep.contains(vitalPackage)).findAny().isPresent(),
294                     String.format(
295                             "Check [%s] package is in the list of required packages %s of [%s] package",
296                             vitalPackage, prerequisites, packageName));
297         } else {
298             TKit.trace(String.format(
299                     "Not cheking %s required packages of [%s] package",
300                     prerequisites, packageName));
301         }
302     }
303 
addBundleDesktopIntegrationVerifier(PackageTest test, boolean integrated)304     static void addBundleDesktopIntegrationVerifier(PackageTest test,
305             boolean integrated) {
306         final String xdgUtils = "xdg-utils";
307 
308         Function<List<String>, String> verifier = (lines) -> {
309             // Lookup for xdg commands
310             return lines.stream().filter(line -> {
311                 Set<String> words = Stream.of(line.split("\\s+")).collect(
312                         Collectors.toSet());
313                 return words.contains("xdg-desktop-menu") || words.contains(
314                         "xdg-mime") || words.contains("xdg-icon-resource");
315             }).findFirst().orElse(null);
316         };
317 
318         test.addBundleVerifier(cmd -> {
319             // Verify dependencies.
320             List<String> prerequisites = getPrerequisitePackages(cmd);
321             boolean xdgUtilsFound = prerequisites.contains(xdgUtils);
322             TKit.assertTrue(xdgUtilsFound == integrated, String.format(
323                     "Check [%s] is%s in the list of required packages %s",
324                     xdgUtils, integrated ? "" : " NOT", prerequisites));
325 
326             Map<Scriptlet, List<String>> scriptlets = getScriptlets(cmd);
327             if (integrated) {
328                 Set<Scriptlet> requiredScriptlets = Stream.of(Scriptlet.values()).sorted().collect(
329                         Collectors.toSet());
330                 TKit.assertTrue(scriptlets.keySet().containsAll(
331                         requiredScriptlets), String.format(
332                                 "Check all required scriptlets %s found in the package. Package scriptlets: %s",
333                                 requiredScriptlets, scriptlets.keySet()));
334             }
335 
336             // Lookup for xdg commands in scriptlets.
337             scriptlets.entrySet().forEach(scriptlet -> {
338                 String lineWithXsdCommand = verifier.apply(scriptlet.getValue());
339                 String assertMsg = String.format(
340                         "Check if [%s] scriptlet uses xdg commands",
341                         scriptlet.getKey());
342                 if (integrated) {
343                     TKit.assertNotNull(lineWithXsdCommand, assertMsg);
344                 } else {
345                     TKit.assertNull(lineWithXsdCommand, assertMsg);
346                 }
347             });
348         });
349 
350         test.addInstallVerifier(cmd -> {
351             // Verify .desktop files.
352             try (var files = Files.walk(cmd.appLayout().destktopIntegrationDirectory(), 1)) {
353                 List<Path> desktopFiles = files
354                         .filter(path -> path.getFileName().toString().endsWith(".desktop"))
355                         .collect(Collectors.toList());
356                 if (!integrated) {
357                     TKit.assertStringListEquals(List.of(),
358                             desktopFiles.stream().map(Path::toString).collect(
359                                     Collectors.toList()),
360                             "Check there are no .desktop files in the package");
361                 }
362                 for (var desktopFile : desktopFiles) {
363                     verifyDesktopFile(cmd, desktopFile);
364                 }
365             }
366         });
367     }
368 
verifyDesktopFile(JPackageCommand cmd, Path desktopFile)369     private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile)
370             throws IOException {
371         TKit.trace(String.format("Check [%s] file BEGIN", desktopFile));
372         List<String> lines = Files.readAllLines(desktopFile);
373         TKit.assertEquals("[Desktop Entry]", lines.get(0), "Check file header");
374 
375         Map<String, String> data = lines.stream()
376         .skip(1)
377         .peek(str -> TKit.assertTextStream("=").predicate(String::contains).apply(Stream.of(str)))
378         .map(str -> {
379             String components[] = str.split("=(?=.+)");
380             if (components.length == 1) {
381                 return Map.entry(str.substring(0, str.length() - 1), "");
382             }
383             return Map.entry(components[0], components[1]);
384         }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> {
385             TKit.assertUnexpected("Multiple values of the same key");
386             return null;
387         }));
388 
389         final Set<String> mandatoryKeys = new HashSet(Set.of("Name", "Comment",
390                 "Exec", "Icon", "Terminal", "Type", "Categories"));
391         mandatoryKeys.removeAll(data.keySet());
392         TKit.assertTrue(mandatoryKeys.isEmpty(), String.format(
393                 "Check for missing %s keys in the file", mandatoryKeys));
394 
395         for (var e : Map.of("Type", "Application", "Terminal", "false").entrySet()) {
396             String key = e.getKey();
397             TKit.assertEquals(e.getValue(), data.get(key), String.format(
398                     "Check value of [%s] key", key));
399         }
400 
401         // Verify value of `Exec` property in .desktop files are escaped if required
402         String launcherPath = data.get("Exec");
403         if (Pattern.compile("\\s").matcher(launcherPath).find()) {
404             TKit.assertTrue(launcherPath.startsWith("\"")
405                     && launcherPath.endsWith("\""),
406                     "Check path to the launcher is enclosed in double quotes");
407             launcherPath = launcherPath.substring(1, launcherPath.length() - 1);
408         }
409 
410         Stream.of(launcherPath, data.get("Icon"))
411                 .map(Path::of)
412                 .map(cmd::pathToUnpackedPackageFile)
413                 .forEach(TKit::assertFileExists);
414 
415         TKit.trace(String.format("Check [%s] file END", desktopFile));
416     }
417 
initFileAssociationsTestFile(Path testFile)418     static void initFileAssociationsTestFile(Path testFile) {
419         try {
420             // Write something in test file.
421             // On Ubuntu and Oracle Linux empty files are considered
422             // plain text. Seems like a system bug.
423             //
424             // $ >foo.jptest1
425             // $ xdg-mime query filetype foo.jptest1
426             // text/plain
427             // $ echo > foo.jptest1
428             // $ xdg-mime query filetype foo.jptest1
429             // application/x-jpackage-jptest1
430             //
431             Files.write(testFile, Arrays.asList(""));
432         } catch (IOException ex) {
433             throw new RuntimeException(ex);
434         }
435     }
436 
getSystemDesktopFilesFolder()437     private static Path getSystemDesktopFilesFolder() {
438         return Stream.of("/usr/share/applications",
439                 "/usr/local/share/applications").map(Path::of).filter(dir -> {
440             return Files.exists(dir.resolve("defaults.list"));
441         }).findFirst().orElseThrow(() -> new RuntimeException(
442                 "Failed to locate system .desktop files folder"));
443     }
444 
445     static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) {
446         test.addInstallVerifier(cmd -> {
447             if (cmd.isPackageUnpacked("Not running file associations checks")) {
448                 return;
449             }
450 
451             PackageTest.withTestFileAssociationsFile(fa, testFile -> {
452                 String mimeType = queryFileMimeType(testFile);
453 
454                 TKit.assertEquals(fa.getMime(), mimeType, String.format(
455                         "Check mime type of [%s] file", testFile));
456 
457                 String desktopFileName = queryMimeTypeDefaultHandler(mimeType);
458 
459                 Path desktopFile = getSystemDesktopFilesFolder().resolve(
460                         desktopFileName);
461 
462                 TKit.assertFileExists(desktopFile);
463 
464                 TKit.trace(String.format("Reading [%s] file...", desktopFile));
465                 String mimeHandler = Files.readAllLines(desktopFile).stream().peek(
466                         v -> TKit.trace(v)).filter(
467                                 v -> v.startsWith("Exec=")).map(
468                                 v -> v.split("=", 2)[1]).findFirst().orElseThrow();
469 
470                 TKit.trace(String.format("Done"));
471 
472                 TKit.assertEquals(cmd.appLauncherPath().toString(),
473                         mimeHandler, String.format(
474                                 "Check mime type handler is the main application launcher"));
475 
476             });
477         });
478 
479         test.addUninstallVerifier(cmd -> {
480             PackageTest.withTestFileAssociationsFile(fa, testFile -> {
481                 String mimeType = queryFileMimeType(testFile);
482 
483                 TKit.assertNotEquals(fa.getMime(), mimeType, String.format(
484                         "Check mime type of [%s] file", testFile));
485 
486                 String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime());
487 
488                 TKit.assertNull(desktopFileName, String.format(
489                         "Check there is no default handler for [%s] mime type",
490                         fa.getMime()));
491             });
492         });
493 
494         test.addBundleVerifier(cmd -> {
495             final Path mimeTypeIconFileName = fa.getLinuxIconFileName();
496             if (mimeTypeIconFileName != null) {
497                 // Verify there are xdg registration commands for mime icon file.
498                 Path mimeTypeIcon = cmd.appLayout().destktopIntegrationDirectory().resolve(
499                         mimeTypeIconFileName);
500 
501                 Map<Scriptlet, List<String>> scriptlets = getScriptlets(cmd);
502                 scriptlets.entrySet().stream().forEach(e -> verifyIconInScriptlet(
503                         e.getKey(), e.getValue(), mimeTypeIcon));
504             }
505         });
506     }
507 
508     private static String queryFileMimeType(Path file) {
509         return Executor.of("xdg-mime", "query", "filetype").addArgument(file)
510                 .executeAndGetFirstLineOfOutput();
511     }
512 
513     private static String queryMimeTypeDefaultHandler(String mimeType) {
514         return Executor.of("xdg-mime", "query", "default", mimeType)
515                 .executeAndGetFirstLineOfOutput();
516     }
517 
518     private static void verifyIconInScriptlet(Scriptlet scriptletType,
519             List<String> scriptletBody, Path iconPathInPackage) {
520         final String dashMime = IOUtils.replaceSuffix(
521                 iconPathInPackage.getFileName(), null).toString();
522         final String xdgCmdName = "xdg-icon-resource";
523 
524         Stream<String> scriptletBodyStream = scriptletBody.stream()
525                 .filter(str -> str.startsWith(xdgCmdName))
526                 .filter(str -> Pattern.compile(
527                         "\\b" + dashMime + "\\b").matcher(str).find());
528         if (scriptletType == Scriptlet.PostInstall) {
529             scriptletBodyStream = scriptletBodyStream.filter(str -> List.of(
530                     str.split("\\s+")).contains(iconPathInPackage.toString()));
531         }
532 
533         scriptletBodyStream.peek(xdgCmd -> {
534             Matcher m = XDG_CMD_ICON_SIZE_PATTERN.matcher(xdgCmd);
535             TKit.assertTrue(m.find(), String.format(
536                     "Check icon size is specified as a number in [%s] xdg command of [%s] scriptlet",
537                     xdgCmd, scriptletType));
538             int iconSize = Integer.parseInt(m.group(1));
539             TKit.assertTrue(XDG_CMD_VALID_ICON_SIZES.contains(iconSize),
540                     String.format(
541                             "Check icon size [%s] is one of %s values",
542                             iconSize, XDG_CMD_VALID_ICON_SIZES));
543         })
544         .findFirst().orElseGet(() -> {
545             TKit.assertUnexpected(String.format(
546                     "Failed to find [%s] command in [%s] scriptlet for [%s] icon file",
547                     xdgCmdName, scriptletType, iconPathInPackage));
548             return null;
549         });
550     }
551 
552     private static Map<Scriptlet, List<String>> getScriptlets(
553             JPackageCommand cmd, Scriptlet... scriptlets) {
554         cmd.verifyIsOfType(PackageType.LINUX);
555 
556         Set<Scriptlet> scriptletSet = Set.of(
557                 scriptlets.length == 0 ? Scriptlet.values() : scriptlets);
558         switch (cmd.packageType()) {
559             case LINUX_DEB:
560                 return getDebScriptlets(cmd, scriptletSet);
561 
562             case LINUX_RPM:
563                 return getRpmScriptlets(cmd, scriptletSet);
564         }
565 
566         // Unreachable
567         return null;
568     }
569 
570     private static Map<Scriptlet, List<String>> getDebScriptlets(
571             JPackageCommand cmd, Set<Scriptlet> scriptlets) {
572         Map<Scriptlet, List<String>> result = new HashMap<>();
573         TKit.withTempDirectory("dpkg-control-files", tempDir -> {
574             // Extract control Debian package files into temporary directory
575             Executor.of("dpkg", "-e")
576                     .addArgument(cmd.outputBundle())
577                     .addArgument(tempDir)
578                     .execute();
579 
580             for (Scriptlet scriptlet : scriptlets) {
581                 Path controlFile = Path.of(scriptlet.deb);
582                 result.put(scriptlet, Files.readAllLines(tempDir.resolve(
583                         controlFile)));
584             }
585         });
586         return result;
587     }
588 
589     private static Map<Scriptlet, List<String>> getRpmScriptlets(
590             JPackageCommand cmd, Set<Scriptlet> scriptlets) {
591         List<String> output = Executor.of("rpm", "-qp", "--scripts",
592                 cmd.outputBundle().toString()).executeAndGetOutput();
593 
594         Map<Scriptlet, List<String>> result = new HashMap<>();
595         List<String> curScriptletBody = null;
596         for (String str : output) {
597             Matcher m = Scriptlet.RPM_HEADER_PATTERN.matcher(str);
598             if (m.find()) {
599                 Scriptlet scriptlet = Scriptlet.RPM_MAP.get(m.group(1));
600                 if (scriptlets.contains(scriptlet)) {
601                     curScriptletBody = new ArrayList<>();
602                     result.put(scriptlet, curScriptletBody);
603                 } else if (curScriptletBody != null) {
604                     curScriptletBody = null;
605                 }
606             } else if (curScriptletBody != null) {
607                 curScriptletBody.add(str);
608             }
609         }
610 
611         return result;
612     }
613 
614     private static enum Scriptlet {
615         PostInstall("postinstall", "postinst"),
616         PreUninstall("preuninstall", "prerm");
617 
618         Scriptlet(String rpm, String deb) {
619             this.rpm = rpm;
620             this.deb = deb;
621         }
622 
623         private final String rpm;
624         private final String deb;
625 
626         static final Pattern RPM_HEADER_PATTERN = Pattern.compile(String.format(
627                 "(%s) scriptlet \\(using /bin/sh\\):", Stream.of(values()).map(
628                         v -> v.rpm).collect(Collectors.joining("|"))));
629 
630         static final Map<String, Scriptlet> RPM_MAP = Stream.of(values()).collect(
631                 Collectors.toMap(v -> v.rpm, v -> v));
632     };
633 
634     public static String getDefaultPackageArch(PackageType type) {
635         if (archs == null) {
636             archs = new HashMap<>();
637         }
638 
639         String arch = archs.get(type);
640         if (arch == null) {
641             Executor exec = null;
642             switch (type) {
643                 case LINUX_DEB:
644                     exec = Executor.of("dpkg", "--print-architecture");
645                     break;
646 
647                 case LINUX_RPM:
648                     exec = Executor.of("rpmbuild", "--eval=%{_target_cpu}");
649                     break;
650             }
651             arch = exec.executeAndGetFirstLineOfOutput();
652             archs.put(type, arch);
653         }
654         return arch;
655     }
656 
657     static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
658             "lib/server/libjvm.so"));
659 
660     private static Map<PackageType, String> archs;
661 
662     private final static Pattern XDG_CMD_ICON_SIZE_PATTERN = Pattern.compile("\\s--size\\s+(\\d+)\\b");
663 
664     // Values grabbed from https://linux.die.net/man/1/xdg-icon-resource
665     private final static Set<Integer> XDG_CMD_VALID_ICON_SIZES = Set.of(16, 22, 32, 48, 64, 128);
666 }
667