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