1 /*
   2  * Copyright (c) 2019, 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.*;
  29 import java.util.function.Function;
  30 import java.util.stream.Collectors;
  31 import java.util.stream.Stream;
  32 import jdk.jpackage.test.PackageTest.PackageHandlers;
  33 
  34 public class LinuxHelper {
  35     private static String getRelease(JPackageCommand cmd) {
  36         return cmd.getArgumentValue("--linux-app-release", () -> "1");
  37     }
  38 
  39     public static String getPackageName(JPackageCommand cmd) {
  40         cmd.verifyIsOfType(PackageType.LINUX);
  41         return cmd.getArgumentValue("--linux-package-name",
  42                 () -> cmd.name().toLowerCase());
  43     }
  44 
  45     public static Path getDesktopFile(JPackageCommand cmd) {
  46         return getDesktopFile(cmd, null);
  47     }
  48 
  49     public static Path getDesktopFile(JPackageCommand cmd, String launcherName) {
  50         cmd.verifyIsOfType(PackageType.LINUX);
  51         String desktopFileName = String.format("%s-%s.desktop", getPackageName(
  52                 cmd), Optional.ofNullable(launcherName).orElseGet(
  53                         () -> cmd.name()));
  54         return cmd.appLayout().destktopIntegrationDirectory().resolve(
  55                 desktopFileName);
  56     }
  57 
  58     static String getBundleName(JPackageCommand cmd) {
  59         cmd.verifyIsOfType(PackageType.LINUX);
  60 
  61         final PackageType packageType = cmd.packageType();
  62         String format = null;
  63         switch (packageType) {
  64             case LINUX_DEB:
  65                 format = "%s_%s-%s_%s";
  66                 break;
  67 
  68             case LINUX_RPM:
  69                 format = "%s-%s-%s.%s";
  70                 break;
  71         }
  72 
  73         final String release = getRelease(cmd);
  74         final String version = cmd.version();
  75 
  76         return String.format(format, getPackageName(cmd), version, release,
  77                 getDefaultPackageArch(packageType)) + packageType.getSuffix();
  78     }
  79 
  80     public static Stream<Path> getPackageFiles(JPackageCommand cmd) {
  81         cmd.verifyIsOfType(PackageType.LINUX);
  82 
  83         final PackageType packageType = cmd.packageType();
  84         final Path packageFile = cmd.outputBundle();
  85 
  86         Executor exec = null;
  87         switch (packageType) {
  88             case LINUX_DEB:
  89                 exec = Executor.of("dpkg", "--contents").addArgument(packageFile);
  90                 break;
  91 
  92             case LINUX_RPM:
  93                 exec = Executor.of("rpm", "-qpl").addArgument(packageFile);
  94                 break;
  95         }
  96 
  97         Stream<String> lines = exec.executeAndGetOutput().stream();
  98         if (packageType == PackageType.LINUX_DEB) {
  99             // Typical text lines produced by dpkg look like:
 100             // drwxr-xr-x root/root         0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/
 101             // -rw-r--r-- root/root    574912 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/libmlib_image.so
 102             // Need to skip all fields but absolute path to file.
 103             lines = lines.map(line -> line.substring(line.indexOf(" ./") + 2));
 104         }
 105         return lines.map(Path::of);
 106     }
 107 
 108     public static List<String> getPrerequisitePackages(JPackageCommand cmd) {
 109         cmd.verifyIsOfType(PackageType.LINUX);
 110         var packageType = cmd.packageType();
 111         switch (packageType) {
 112             case LINUX_DEB:
 113                 return Stream.of(getDebBundleProperty(cmd.outputBundle(),
 114                         "Depends").split(",")).map(String::strip).collect(
 115                         Collectors.toList());
 116 
 117             case LINUX_RPM:
 118                 return Executor.of("rpm", "-qp", "-R")
 119                 .addArgument(cmd.outputBundle())
 120                 .executeAndGetOutput();
 121         }
 122         // Unreachable
 123         return null;
 124     }
 125 
 126     public static String getBundleProperty(JPackageCommand cmd,
 127             String propertyName) {
 128         return getBundleProperty(cmd,
 129                 Map.of(PackageType.LINUX_DEB, propertyName,
 130                         PackageType.LINUX_RPM, propertyName));
 131     }
 132 
 133     public static String getBundleProperty(JPackageCommand cmd,
 134             Map<PackageType, String> propertyName) {
 135         cmd.verifyIsOfType(PackageType.LINUX);
 136         var packageType = cmd.packageType();
 137         switch (packageType) {
 138             case LINUX_DEB:
 139                 return getDebBundleProperty(cmd.outputBundle(), propertyName.get(
 140                         packageType));
 141 
 142             case LINUX_RPM:
 143                 return getRpmBundleProperty(cmd.outputBundle(), propertyName.get(
 144                         packageType));
 145         }
 146         // Unrechable
 147         return null;
 148     }
 149 
 150     static PackageHandlers createDebPackageHandlers() {
 151         PackageHandlers deb = new PackageHandlers();
 152         deb.installHandler = cmd -> {
 153             cmd.verifyIsOfType(PackageType.LINUX_DEB);
 154             Executor.of("sudo", "dpkg", "-i")
 155             .addArgument(cmd.outputBundle())
 156             .execute();
 157         };
 158         deb.uninstallHandler = cmd -> {
 159             cmd.verifyIsOfType(PackageType.LINUX_DEB);
 160             Executor.of("sudo", "dpkg", "-r", getPackageName(cmd)).execute();
 161         };
 162         deb.unpackHandler = (cmd, destinationDir) -> {
 163             cmd.verifyIsOfType(PackageType.LINUX_DEB);
 164             Executor.of("dpkg", "-x")
 165             .addArgument(cmd.outputBundle())
 166             .addArgument(destinationDir)
 167             .execute();
 168             return destinationDir.resolve(String.format(".%s",
 169                     cmd.appInstallationDirectory())).normalize();
 170         };
 171         return deb;
 172     }
 173 
 174     static PackageHandlers createRpmPackageHandlers() {
 175         PackageHandlers rpm = new PackageHandlers();
 176         rpm.installHandler = cmd -> {
 177             cmd.verifyIsOfType(PackageType.LINUX_RPM);
 178             Executor.of("sudo", "rpm", "-i")
 179             .addArgument(cmd.outputBundle())
 180             .execute();
 181         };
 182         rpm.uninstallHandler = cmd -> {
 183             cmd.verifyIsOfType(PackageType.LINUX_RPM);
 184             Executor.of("sudo", "rpm", "-e", getPackageName(cmd)).execute();
 185         };
 186         rpm.unpackHandler = (cmd, destinationDir) -> {
 187             cmd.verifyIsOfType(PackageType.LINUX_RPM);
 188             Executor.of("sh", "-c", String.format(
 189                     "rpm2cpio '%s' | cpio -idm --quiet",
 190                     JPackageCommand.escapeAndJoin(
 191                             cmd.outputBundle().toAbsolutePath().toString())))
 192             .setDirectory(destinationDir)
 193             .execute();
 194             return destinationDir.resolve(String.format(".%s",
 195                     cmd.appInstallationDirectory())).normalize();
 196         };
 197 
 198         return rpm;
 199     }
 200 
 201     static Path getLauncherPath(JPackageCommand cmd) {
 202         cmd.verifyIsOfType(PackageType.LINUX);
 203 
 204         final String launcherName = cmd.name();
 205         final String launcherRelativePath = Path.of("/bin", launcherName).toString();
 206 
 207         return getPackageFiles(cmd).filter(path -> path.toString().endsWith(
 208                 launcherRelativePath)).findFirst().or(() -> {
 209             TKit.assertUnexpected(String.format(
 210                     "Failed to find %s in %s package", launcherName,
 211                     getPackageName(cmd)));
 212             return null;
 213         }).get();
 214     }
 215 
 216     static long getInstalledPackageSizeKB(JPackageCommand cmd) {
 217         cmd.verifyIsOfType(PackageType.LINUX);
 218 
 219         final Path packageFile = cmd.outputBundle();
 220         switch (cmd.packageType()) {
 221             case LINUX_DEB:
 222                 return Long.parseLong(getDebBundleProperty(packageFile,
 223                         "Installed-Size"));
 224 
 225             case LINUX_RPM:
 226                 return Long.parseLong(getRpmBundleProperty(packageFile, "Size")) >> 10;
 227         }
 228 
 229         return 0;
 230     }
 231 
 232     static String getDebBundleProperty(Path bundle, String fieldName) {
 233         return Executor.of("dpkg-deb", "-f")
 234                 .addArgument(bundle)
 235                 .addArgument(fieldName)
 236                 .executeAndGetFirstLineOfOutput();
 237     }
 238 
 239     static String getRpmBundleProperty(Path bundle, String fieldName) {
 240         return Executor.of("rpm", "-qp", "--queryformat", String.format("%%{%s}", fieldName))
 241                 .addArgument(bundle)
 242                 .executeAndGetFirstLineOfOutput();
 243     }
 244 
 245     static void verifyPackageBundleEssential(JPackageCommand cmd) {
 246         String packageName = LinuxHelper.getPackageName(cmd);
 247         TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
 248                 cmd), String.format(
 249                         "Check installed size of [%s] package in KB is not zero",
 250                         packageName));
 251 
 252         final boolean checkPrerequisites;
 253         if (cmd.isRuntime()) {
 254             Path runtimeDir = cmd.appRuntimeDirectory();
 255             Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map(
 256                     runtimeDir::resolve).collect(Collectors.toSet());
 257             Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter(
 258                     expectedCriticalRuntimePaths::contains).collect(
 259                             Collectors.toSet());
 260             checkPrerequisites = expectedCriticalRuntimePaths.equals(
 261                     actualCriticalRuntimePaths);
 262         } else {
 263             checkPrerequisites = true;
 264         }
 265 
 266         List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd);
 267         if (checkPrerequisites) {
 268             final String vitalPackage = "libc";
 269             TKit.assertTrue(prerequisites.stream().filter(
 270                     dep -> dep.contains(vitalPackage)).findAny().isPresent(),
 271                     String.format(
 272                             "Check [%s] package is in the list of required packages %s of [%s] package",
 273                             vitalPackage, prerequisites, packageName));
 274         } else {
 275             TKit.trace(String.format(
 276                     "Not cheking %s required packages of [%s] package",
 277                     prerequisites, packageName));
 278         }
 279     }
 280 
 281     static void addBundleDesktopIntegrationVerifier(PackageTest test,
 282             boolean integrated) {
 283         final String xdgUtils = "xdg-utils";
 284 
 285         test.addBundleVerifier(cmd -> {
 286             List<String> prerequisites = getPrerequisitePackages(cmd);
 287             boolean xdgUtilsFound = prerequisites.contains(xdgUtils);
 288             if (integrated) {
 289                 TKit.assertTrue(xdgUtilsFound, String.format(
 290                         "Check [%s] is in the list of required packages %s",
 291                         xdgUtils, prerequisites));
 292             } else {
 293                 TKit.assertFalse(xdgUtilsFound, String.format(
 294                         "Check [%s] is NOT in the list of required packages %s",
 295                         xdgUtils, prerequisites));
 296             }
 297         });
 298 
 299         test.forTypes(PackageType.LINUX_DEB, () -> {
 300             addDebBundleDesktopIntegrationVerifier(test, integrated);
 301         });
 302     }
 303 
 304     private static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
 305             boolean integrated) {
 306         Function<List<String>, String> verifier = (lines) -> {
 307             // Lookup for xdg commands
 308             return lines.stream().filter(line -> {
 309                 Set<String> words = Stream.of(line.split("\\s+")).collect(
 310                         Collectors.toSet());
 311                 return words.contains("xdg-desktop-menu") || words.contains(
 312                         "xdg-mime") || words.contains("xdg-icon-resource");
 313             }).findFirst().orElse(null);
 314         };
 315 
 316         test.addBundleVerifier(cmd -> {
 317             TKit.withTempDirectory("dpkg-control-files", tempDir -> {
 318                 // Extract control Debian package files into temporary directory
 319                 Executor.of("dpkg", "-e")
 320                 .addArgument(cmd.outputBundle())
 321                 .addArgument(tempDir)
 322                 .execute();
 323 
 324                 Path controlFile = Path.of("postinst");
 325 
 326                 // Lookup for xdg commands in postinstall script
 327                 String lineWithXsdCommand = verifier.apply(
 328                         Files.readAllLines(tempDir.resolve(controlFile)));
 329                 String assertMsg = String.format(
 330                         "Check if %s@%s control file uses xdg commands",
 331                         cmd.outputBundle(), controlFile);
 332                 if (integrated) {
 333                     TKit.assertNotNull(lineWithXsdCommand, assertMsg);
 334                 } else {
 335                     TKit.assertNull(lineWithXsdCommand, assertMsg);
 336                 }
 337             });
 338         });
 339     }
 340 
 341     static void initFileAssociationsTestFile(Path testFile) {
 342         try {
 343             // Write something in test file.
 344             // On Ubuntu and Oracle Linux empty files are considered
 345             // plain text. Seems like a system bug.
 346             //
 347             // $ >foo.jptest1
 348             // $ xdg-mime query filetype foo.jptest1
 349             // text/plain
 350             // $ echo > foo.jptest1
 351             // $ xdg-mime query filetype foo.jptest1
 352             // application/x-jpackage-jptest1
 353             //
 354             Files.write(testFile, Arrays.asList(""));
 355         } catch (IOException ex) {
 356             throw new RuntimeException(ex);
 357         }
 358     }
 359 
 360     private static Path getSystemDesktopFilesFolder() {
 361         return Stream.of("/usr/share/applications",
 362                 "/usr/local/share/applications").map(Path::of).filter(dir -> {
 363             return Files.exists(dir.resolve("defaults.list"));
 364         }).findFirst().orElseThrow(() -> new RuntimeException(
 365                 "Failed to locate system .desktop files folder"));
 366     }
 367 
 368     static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) {
 369         test.addInstallVerifier(cmd -> {
 370             if (cmd.isPackageUnpacked("Not running file associations checks")) {
 371                 return;
 372             }
 373 
 374             PackageTest.withTestFileAssociationsFile(fa, testFile -> {
 375                 String mimeType = queryFileMimeType(testFile);
 376 
 377                 TKit.assertEquals(fa.getMime(), mimeType, String.format(
 378                         "Check mime type of [%s] file", testFile));
 379 
 380                 String desktopFileName = queryMimeTypeDefaultHandler(mimeType);
 381 
 382                 Path desktopFile = getSystemDesktopFilesFolder().resolve(
 383                         desktopFileName);
 384 
 385                 TKit.assertFileExists(desktopFile);
 386 
 387                 TKit.trace(String.format("Reading [%s] file...", desktopFile));
 388                 String mimeHandler = Files.readAllLines(desktopFile).stream().peek(
 389                         v -> TKit.trace(v)).filter(
 390                                 v -> v.startsWith("Exec=")).map(
 391                                 v -> v.split("=", 2)[1]).findFirst().orElseThrow();
 392 
 393                 TKit.trace(String.format("Done"));
 394 
 395                 TKit.assertEquals(cmd.appLauncherPath().toString(),
 396                         mimeHandler, String.format(
 397                                 "Check mime type handler is the main application launcher"));
 398 
 399             });
 400         });
 401 
 402         test.addUninstallVerifier(cmd -> {
 403             PackageTest.withTestFileAssociationsFile(fa, testFile -> {
 404                 String mimeType = queryFileMimeType(testFile);
 405 
 406                 TKit.assertNotEquals(fa.getMime(), mimeType, String.format(
 407                         "Check mime type of [%s] file", testFile));
 408 
 409                 String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime());
 410 
 411                 TKit.assertNull(desktopFileName, String.format(
 412                         "Check there is no default handler for [%s] mime type",
 413                         fa.getMime()));
 414             });
 415         });
 416     }
 417 
 418     private static String queryFileMimeType(Path file) {
 419         return Executor.of("xdg-mime", "query", "filetype").addArgument(file)
 420                 .executeAndGetFirstLineOfOutput();
 421     }
 422 
 423     private static String queryMimeTypeDefaultHandler(String mimeType) {
 424         return Executor.of("xdg-mime", "query", "default", mimeType)
 425                 .executeAndGetFirstLineOfOutput();
 426     }
 427 
 428     public static String getDefaultPackageArch(PackageType type) {
 429         if (archs == null) {
 430             archs = new HashMap<>();
 431         }
 432 
 433         String arch = archs.get(type);
 434         if (arch == null) {
 435             Executor exec = null;
 436             switch (type) {
 437                 case LINUX_DEB:
 438                     exec = Executor.of("dpkg", "--print-architecture");
 439                     break;
 440 
 441                 case LINUX_RPM:
 442                     exec = Executor.of("rpmbuild", "--eval=%{_target_cpu}");
 443                     break;
 444             }
 445             arch = exec.executeAndGetFirstLineOfOutput();
 446             archs.put(type, arch);
 447         }
 448         return arch;
 449     }
 450 
 451     static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
 452             "lib/server/libjvm.so"));
 453 
 454     static private Map<PackageType, String> archs;
 455 }