1 /*
   2  * Copyright (c) 2017, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.*;
  29 import java.nio.charset.Charset;
  30 import java.nio.file.Files;
  31 import java.text.MessageFormat;
  32 import java.util.*;
  33 
  34 import static jdk.jpackage.internal.WindowsBundlerParam.*;
  35 
  36 public class WinExeBundler extends AbstractBundler {
  37 
  38     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  39             "jdk.jpackage.internal.resources.WinResources");
  40 
  41     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
  42             new WindowsBundlerParam<>(
  43             "win.app.bundler",
  44             WinAppBundler.class,
  45             params -> new WinAppBundler(),
  46             null);
  47 
  48     public static final BundlerParamInfo<File> EXE_IMAGE_DIR =
  49             new WindowsBundlerParam<>(
  50             "win.exe.imageDir",
  51             File.class,
  52             params -> {
  53                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  54                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  55                 return new File(imagesRoot, "win-exe.image");
  56             },
  57             (s, p) -> null);
  58 
  59     public static final BundlerParamInfo<File> WIN_APP_IMAGE =
  60             new WindowsBundlerParam<>(
  61             "win.app.image",
  62             File.class,
  63             null,
  64             (s, p) -> null);
  65 
  66     public static final BundlerParamInfo<UUID> UPGRADE_UUID =
  67             new WindowsBundlerParam<>(
  68             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
  69             UUID.class,
  70             params -> UUID.randomUUID(),
  71             (s, p) -> UUID.fromString(s));
  72 
  73     public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE  =
  74             new StandardBundlerParam<>(
  75             Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
  76             Boolean.class,
  77             params -> true, // default to system wide
  78             (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
  79                     : Boolean.valueOf(s)
  80             );
  81     public static final StandardBundlerParam<String> PRODUCT_VERSION =
  82             new StandardBundlerParam<>(
  83                     "win.msi.productVersion",
  84                     String.class,
  85                     VERSION::fetchFrom,
  86                     (s, p) -> s
  87             );
  88 
  89     public static final StandardBundlerParam<Boolean> MENU_HINT =
  90         new WindowsBundlerParam<>(
  91                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
  92                 Boolean.class,
  93                 params -> false,
  94                 (s, p) -> (s == null ||
  95                         "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
  96         );
  97 
  98     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
  99         new WindowsBundlerParam<>(
 100                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
 101                 Boolean.class,
 102                 params -> false,
 103                 (s, p) -> (s == null ||
 104                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 105         );
 106 
 107     private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss";
 108     private final static String DEFAULT_JRE_EXE_TEMPLATE = "template.jre.iss";
 109     private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe";
 110 
 111     public static final BundlerParamInfo<String>
 112             TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>(
 113             "win.exe.iscc.exe",
 114             String.class,
 115             params -> {
 116                 for (String dirString : (System.getenv("PATH")
 117                         + ";C:\\Program Files (x86)\\Inno Setup 5;"
 118                         + "C:\\Program Files\\Inno Setup 5").split(";")) {
 119                     File f = new File(dirString.replace("\"", ""),
 120                             TOOL_INNO_SETUP_COMPILER);
 121                     if (f.isFile()) {
 122                         return f.toString();
 123                     }
 124                 }
 125                 return null;
 126             },
 127             null);
 128 
 129     @Override
 130     public String getName() {
 131         return getString("exe.bundler.name");
 132     }
 133 
 134     @Override
 135     public String getDescription() {
 136         return getString("exe.bundler.description");
 137     }
 138 
 139     @Override
 140     public String getID() {
 141         return "exe";
 142     }
 143 
 144     @Override
 145     public String getBundleType() {
 146         return "INSTALLER";
 147     }
 148 
 149     @Override
 150     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 151         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 152         results.addAll(WinAppBundler.getAppBundleParameters());
 153         results.addAll(getExeBundleParameters());
 154         return results;
 155     }
 156 
 157     public static Collection<BundlerParamInfo<?>> getExeBundleParameters() {
 158         return Arrays.asList(
 159                 DESCRIPTION,
 160                 COPYRIGHT,
 161                 LICENSE_FILE,
 162                 MENU_GROUP,
 163                 MENU_HINT,
 164                 SHORTCUT_HINT,
 165                 EXE_SYSTEM_WIDE,
 166                 VENDOR,
 167                 INSTALLDIR_CHOOSER
 168         );
 169     }
 170 
 171     @Override
 172     public File execute(Map<String, ? super Object> params,
 173             File outputParentDir) throws PackagerException {
 174         return bundle(params, outputParentDir);
 175     }
 176 
 177     @Override
 178     public boolean supported(boolean platformInstaller) {
 179         return (Platform.getPlatform() == Platform.WINDOWS);
 180     }
 181 
 182     private static String findToolVersion(String toolName) {
 183         try {
 184             if (toolName == null || "".equals(toolName)) return null;
 185 
 186             ProcessBuilder pb = new ProcessBuilder(
 187                     toolName,
 188                     "/?");
 189             VersionExtractor ve =
 190                     new VersionExtractor("Inno Setup (\\d+.?\\d*)");
 191             IOUtils.exec(pb, true, ve);
 192             // not interested in the output
 193             String version = ve.getVersion();
 194             Log.verbose(MessageFormat.format(
 195                     getString("message.tool-version"), toolName, version));
 196             return version;
 197         } catch (Exception e) {
 198             if (Log.isDebug()) {
 199                 Log.verbose(e);
 200             }
 201             return null;
 202         }
 203     }
 204 
 205     @Override
 206     public boolean validate(Map<String, ? super Object> params)
 207             throws UnsupportedPlatformException, ConfigException {
 208         try {
 209             if (params == null) throw new ConfigException(
 210                       getString("error.parameters-null"),
 211                       getString("error.parameters-null.advice"));
 212 
 213             // run basic validation to ensure requirements are met
 214             // we are not interested in return code, only possible exception
 215             APP_BUNDLER.fetchFrom(params).validate(params);
 216 
 217             // make sure some key values don't have newlines
 218             for (BundlerParamInfo<String> pi : Arrays.asList(
 219                     APP_NAME,
 220                     COPYRIGHT,
 221                     DESCRIPTION,
 222                     MENU_GROUP,
 223                     VENDOR,
 224                     VERSION)
 225             ) {
 226                 String v = pi.fetchFrom(params);
 227                 if (v.contains("\n") | v.contains("\r")) {
 228                     throw new ConfigException("Parmeter '" + pi.getID() +
 229                             "' cannot contain a newline.",
 230                             " Change the value of '" + pi.getID() +
 231                             " so that it does not contain any newlines");
 232                 }
 233             }
 234 
 235             // exe bundlers trim the copyright to 100 characters,
 236             // tell them this will happen
 237             if (COPYRIGHT.fetchFrom(params).length() > 100) {
 238                 throw new ConfigException(
 239                         getString("error.copyright-is-too-long"),
 240                         getString("error.copyright-is-too-long.advice"));
 241             }
 242 
 243             String innoVersion = findToolVersion(
 244                     TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params));
 245 
 246             //Inno Setup 5+ is required
 247             String minVersion = "5.0";
 248 
 249             if (VersionExtractor.isLessThan(innoVersion, minVersion)) {
 250                 Log.error(MessageFormat.format(
 251                         getString("message.tool-wrong-version"),
 252                         TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion));
 253                 throw new ConfigException(
 254                         getString("error.iscc-not-found"),
 255                         getString("error.iscc-not-found.advice"));
 256             }
 257 
 258             /********* validate bundle parameters *************/
 259 
 260             // only one mime type per association, at least one file extension
 261             List<Map<String, ? super Object>> associations =
 262                     FILE_ASSOCIATIONS.fetchFrom(params);
 263             if (associations != null) {
 264                 for (int i = 0; i < associations.size(); i++) {
 265                     Map<String, ? super Object> assoc = associations.get(i);
 266                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 267                     if (mimes.size() > 1) {
 268                         throw new ConfigException(MessageFormat.format(
 269                                 getString("error.too-many-content-"
 270                                 + "types-for-file-association"), i),
 271                                 getString("error.too-many-content-"
 272                                 + "types-for-file-association.advice"));
 273                     }
 274                 }
 275             }
 276 
 277             return true;
 278         } catch (RuntimeException re) {
 279             if (re.getCause() instanceof ConfigException) {
 280                 throw (ConfigException) re.getCause();
 281             } else {
 282                 throw new ConfigException(re);
 283             }
 284         }
 285     }
 286 
 287     private boolean prepareProto(Map<String, ? super Object> params)
 288                 throws PackagerException, IOException {
 289         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
 290         File appDir = null;
 291 
 292         // we either have an application image or need to build one
 293         if (appImage != null) {
 294             appDir = new File(EXE_IMAGE_DIR.fetchFrom(params),
 295                 APP_NAME.fetchFrom(params));
 296             // copy everything from appImage dir into appDir/name
 297             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
 298         } else {
 299             appDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
 300                     EXE_IMAGE_DIR.fetchFrom(params), true);
 301         }
 302 
 303         if (appDir == null) {
 304             return false;
 305         }
 306 
 307         params.put(WIN_APP_IMAGE.getID(), appDir);
 308 
 309         String licenseFile = LICENSE_FILE.fetchFrom(params);
 310         if (licenseFile != null) {
 311             // need to copy license file to the working directory and convert to rtf if needed
 312             File lfile = new File(licenseFile);
 313             File destFile = new File(CONFIG_ROOT.fetchFrom(params),
 314                     lfile.getName());
 315             IOUtils.copyFile(lfile, destFile);
 316             ensureByMutationFileIsRTF(destFile);
 317         }
 318 
 319         // copy file association icons
 320         List<Map<String, ? super Object>> fileAssociations =
 321                 FILE_ASSOCIATIONS.fetchFrom(params);
 322 
 323         for (Map<String, ? super Object> fa : fileAssociations) {
 324             File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
 325             if (icon == null) {
 326                 continue;
 327             }
 328 
 329             File faIconFile = new File(appDir, icon.getName());
 330 
 331             if (icon.exists()) {
 332                 try {
 333                     IOUtils.copyFile(icon, faIconFile);
 334                 } catch (IOException e) {
 335                     Log.verbose(e);
 336                 }
 337             }
 338         }
 339 
 340         return true;
 341     }
 342 
 343     public File bundle(Map<String, ? super Object> params, File outdir)
 344             throws PackagerException {
 345         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 346             throw new PackagerException("error.cannot-create-output-dir",
 347                     outdir.getAbsolutePath());
 348         }
 349         if (!outdir.canWrite()) {
 350             throw new PackagerException("error.cannot-write-to-output-dir",
 351                     outdir.getAbsolutePath());
 352         }
 353 
 354         String tempDirectory = WindowsDefender.getUserTempDirectory();
 355         if (Arguments.CLIOptions.context().userProvidedBuildRoot) {
 356             tempDirectory = TEMP_ROOT.fetchFrom(params).getAbsolutePath();
 357         }
 358         if (WindowsDefender.isThereAPotentialWindowsDefenderIssue(
 359                 tempDirectory)) {
 360             Log.error(MessageFormat.format(
 361                     getString("message.potential.windows.defender.issue"),
 362                     tempDirectory));
 363         }
 364 
 365         // validate we have valid tools before continuing
 366         String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params);
 367         if (iscc == null || !new File(iscc).isFile()) {
 368             Log.verbose(getString("error.iscc-not-found"));
 369             Log.verbose(MessageFormat.format(
 370                     getString("message.iscc-file-string"), iscc));
 371             throw new PackagerException("error.iscc-not-found");
 372         }
 373 
 374         File imageDir = EXE_IMAGE_DIR.fetchFrom(params);
 375         try {
 376             imageDir.mkdirs();
 377 
 378             boolean menuShortcut = MENU_HINT.fetchFrom(params);
 379             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 380             if (!menuShortcut && !desktopShortcut) {
 381                 // both can not be false - user will not find the app
 382                 Log.verbose(getString("message.one-shortcut-required"));
 383                 params.put(MENU_HINT.getID(), true);
 384             }
 385 
 386             if (prepareProto(params) && prepareProjectConfig(params)) {
 387                 File configScript = getConfig_Script(params);
 388                 if (configScript.exists()) {
 389                     Log.verbose(MessageFormat.format(
 390                             getString("message.running-wsh-script"),
 391                             configScript.getAbsolutePath()));
 392                     IOUtils.run("wscript", configScript);
 393                 }
 394                 return buildEXE(params, outdir);
 395             }
 396             return null;
 397         } catch (IOException ex) {
 398             Log.verbose(ex);
 399             throw new PackagerException(ex);
 400         }
 401     }
 402 
 403     // name of post-image script
 404     private File getConfig_Script(Map<String, ? super Object> params) {
 405         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 406                 APP_NAME.fetchFrom(params) + "-post-image.wsf");
 407     }
 408 
 409     private String getAppIdentifier(Map<String, ? super Object> params) {
 410         String nm = UPGRADE_UUID.fetchFrom(params).toString();
 411 
 412         // limitation of innosetup
 413         if (nm.length() > 126) {
 414             Log.error(getString("message-truncating-id"));
 415             nm = nm.substring(0, 126);
 416         }
 417 
 418         return nm;
 419     }
 420 
 421     private String getLicenseFile(Map<String, ? super Object> params) {
 422         String licenseFile = LICENSE_FILE.fetchFrom(params);
 423         if (licenseFile != null) {
 424             File lfile = new File(licenseFile);
 425             File destFile = new File(CONFIG_ROOT.fetchFrom(params),
 426                     lfile.getName());
 427             String filePath = destFile.getAbsolutePath();
 428             if (filePath.contains(" ")) {
 429                 return "\"" + filePath + "\"";
 430             } else {
 431                 return filePath;
 432             }
 433         }
 434 
 435         return null;
 436     }
 437 
 438     void validateValueAndPut(Map<String, String> data, String key,
 439                 BundlerParamInfo<String> param,
 440                 Map<String, ? super Object> params) throws IOException {
 441         String value = param.fetchFrom(params);
 442         if (value.contains("\r") || value.contains("\n")) {
 443             throw new IOException("Configuration Parameter " +
 444                      param.getID() + " cannot contain multiple lines of text");
 445         }
 446         data.put(key, innosetupEscape(value));
 447     }
 448 
 449     private String innosetupEscape(String value) {
 450         if (value == null) {
 451             return "";
 452         }
 453 
 454         if (value.contains("\"") || !value.trim().equals(value)) {
 455             value = "\"" + value.replace("\"", "\"\"") + "\"";
 456         }
 457         return value;
 458     }
 459 
 460     boolean prepareMainProjectFile(Map<String, ? super Object> params)
 461             throws IOException {
 462         Map<String, String> data = new HashMap<>();
 463         data.put("PRODUCT_APP_IDENTIFIER",
 464                 innosetupEscape(getAppIdentifier(params)));
 465 
 466         validateValueAndPut(data, "INSTALL_DIR", WINDOWS_INSTALL_DIR, params);
 467         validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, params);
 468         validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, params);
 469         validateValueAndPut(data, "APPLICATION_VERSION", VERSION, params);
 470         validateValueAndPut(data, "INSTALLER_FILE_NAME",
 471                 INSTALLER_FILE_NAME, params);
 472 
 473         data.put("LAUNCHER_NAME",
 474                 innosetupEscape(WinAppBundler.getAppName(params)));
 475 
 476         data.put("APPLICATION_LAUNCHER_FILENAME",
 477                 innosetupEscape(WinAppBundler.getLauncherName(params)));
 478 
 479         data.put("APPLICATION_DESKTOP_SHORTCUT",
 480                 SHORTCUT_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 481         data.put("APPLICATION_MENU_SHORTCUT",
 482                 MENU_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 483         validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, params);
 484         validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, params);
 485 
 486         data.put("APPLICATION_LICENSE_FILE",
 487                 innosetupEscape(getLicenseFile(params)));
 488         data.put("DISABLE_DIR_PAGE",
 489                 INSTALLDIR_CHOOSER.fetchFrom(params) ? "No" : "Yes");
 490 
 491         Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(params);
 492 
 493         if (isSystemWide) {
 494             data.put("APPLICATION_INSTALL_ROOT", "{pf}");
 495             data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
 496         } else {
 497             data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
 498             data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
 499         }
 500 
 501         data.put("ARCHITECTURE_BIT_MODE", "x64");
 502 
 503         validateValueAndPut(data, "RUN_FILENAME", APP_NAME, params);
 504 
 505         validateValueAndPut(data, "APPLICATION_DESCRIPTION",
 506                 DESCRIPTION, params);
 507 
 508         data.put("APPLICATION_SERVICE", "returnFalse");
 509         data.put("APPLICATION_NOT_SERVICE", "returnFalse");
 510         data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse");
 511         data.put("START_ON_INSTALL", "");
 512         data.put("STOP_ON_UNINSTALL", "");
 513         data.put("RUN_AT_STARTUP", "");
 514 
 515         String imagePathString = WIN_APP_IMAGE
 516                 .fetchFrom(params).toPath().toAbsolutePath().toString();
 517         data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString));
 518         Log.verbose("setting APPLICATION_IMAGE to " +
 519                 innosetupEscape(imagePathString) + " for InnoSetup");
 520 
 521         StringBuilder addLaunchersCfg = new StringBuilder();
 522         for (Map<String, ? super Object>
 523                 launcher : ADD_LAUNCHERS.fetchFrom(params)) {
 524             String application_name = APP_NAME.fetchFrom(launcher);
 525             if (MENU_HINT.fetchFrom(launcher)) {
 526                 // Name: "{group}\APPLICATION_NAME";
 527                 // Filename: "{app}\APPLICATION_NAME.exe";
 528                 // IconFilename: "{app}\APPLICATION_NAME.ico"
 529                 addLaunchersCfg.append("Name: \"{group}\\");
 530                 addLaunchersCfg.append(application_name);
 531                 addLaunchersCfg.append("\"; Filename: \"{app}\\");
 532                 addLaunchersCfg.append(application_name);
 533                 addLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
 534                 addLaunchersCfg.append(application_name);
 535                 addLaunchersCfg.append(".ico\"\r\n");
 536             }
 537             if (SHORTCUT_HINT.fetchFrom(launcher)) {
 538                 // Name: "{commondesktop}\APPLICATION_NAME";
 539                 // Filename: "{app}\APPLICATION_NAME.exe";
 540                 // IconFilename: "{app}\APPLICATION_NAME.ico"
 541                 addLaunchersCfg.append("Name: \"{commondesktop}\\");
 542                 addLaunchersCfg.append(application_name);
 543                 addLaunchersCfg.append("\"; Filename: \"{app}\\");
 544                 addLaunchersCfg.append(application_name);
 545                 addLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
 546                 addLaunchersCfg.append(application_name);
 547                 addLaunchersCfg.append(".ico\"\r\n");
 548             }
 549         }
 550         data.put("ADD_LAUNCHERS", addLaunchersCfg.toString());
 551 
 552         StringBuilder registryEntries = new StringBuilder();
 553         String regName = APP_REGISTRY_NAME.fetchFrom(params);
 554         List<Map<String, ? super Object>> fetchFrom =
 555                 FILE_ASSOCIATIONS.fetchFrom(params);
 556         for (int i = 0; i < fetchFrom.size(); i++) {
 557             Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
 558             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 559             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 560 
 561             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 562             String entryName = regName + "File";
 563             if (i > 0) {
 564                 entryName += "." + i;
 565             }
 566 
 567             if (extensions == null) {
 568                 Log.verbose(getString(
 569                         "message.creating-association-with-null-extension"));
 570             } else {
 571                 for (String ext : extensions) {
 572                     if (isSystemWide) {
 573                         // "Root: HKCR; Subkey: \".myp\";
 574                         // ValueType: string; ValueName: \"\";
 575                         // ValueData: \"MyProgramFile\";
 576                         // Flags: uninsdeletevalue"
 577                         registryEntries.append("Root: HKCR; Subkey: \".")
 578                                 .append(ext)
 579                                 .append("\"; ValueType: string;"
 580                                 + " ValueName: \"\"; ValueData: \"")
 581                                 .append(entryName)
 582                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 583                     } else {
 584                         registryEntries.append(
 585                                 "Root: HKCU; Subkey: \"Software\\Classes\\.")
 586                                 .append(ext)
 587                                 .append("\"; ValueType: string;"
 588                                 + " ValueName: \"\"; ValueData: \"")
 589                                 .append(entryName)
 590                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 591                     }
 592                 }
 593             }
 594 
 595             if (extensions != null && !extensions.isEmpty()) {
 596                 String ext = extensions.get(0);
 597                 List<String> mimeTypes =
 598                         FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 599                 for (String mime : mimeTypes) {
 600                     if (isSystemWide) {
 601                         // "Root: HKCR;
 602                         // Subkey: HKCR\\Mime\\Database\\
 603                         //         Content Type\\application/chaos;
 604                         // ValueType: string;
 605                         // ValueName: Extension;
 606                         // ValueData: .chaos;
 607                         // Flags: uninsdeletevalue"
 608                         registryEntries.append("Root: HKCR; Subkey: " +
 609                                  "\"Mime\\Database\\Content Type\\")
 610                             .append(mime)
 611                             .append("\"; ValueType: string; ValueName: " +
 612                                  "\"Extension\"; ValueData: \".")
 613                             .append(ext)
 614                             .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 615                     } else {
 616                         registryEntries.append(
 617                                 "Root: HKCU; Subkey: \"Software\\" +
 618                                 "Classes\\Mime\\Database\\Content Type\\")
 619                                 .append(mime)
 620                                 .append("\"; ValueType: string; " +
 621                                 "ValueName: \"Extension\"; ValueData: \".")
 622                                 .append(ext)
 623                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 624                     }
 625                 }
 626             }
 627 
 628             if (isSystemWide) {
 629                 // "Root: HKCR;
 630                 // Subkey: \"MyProgramFile\";
 631                 // ValueType: string;
 632                 // ValueName: \"\";
 633                 // ValueData: \"My Program File\";
 634                 // Flags: uninsdeletekey"
 635                 registryEntries.append("Root: HKCR; Subkey: \"")
 636                     .append(entryName)
 637                     .append(
 638                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 639                     .append(removeQuotes(description))
 640                     .append("\"; Flags: uninsdeletekey\r\n");
 641             } else {
 642                 registryEntries.append(
 643                     "Root: HKCU; Subkey: \"Software\\Classes\\")
 644                     .append(entryName)
 645                     .append(
 646                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 647                     .append(removeQuotes(description))
 648                     .append("\"; Flags: uninsdeletekey\r\n");
 649             }
 650 
 651             if (icon != null && icon.exists()) {
 652                 if (isSystemWide) {
 653                     // "Root: HKCR;
 654                     // Subkey: \"MyProgramFile\\DefaultIcon\";
 655                     // ValueType: string;
 656                     // ValueName: \"\";
 657                     // ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
 658                     registryEntries.append("Root: HKCR; Subkey: \"")
 659                             .append(entryName)
 660                             .append("\\DefaultIcon\"; ValueType: string; " +
 661                             "ValueName: \"\"; ValueData: \"{app}\\")
 662                             .append(icon.getName())
 663                             .append("\"\r\n");
 664                 } else {
 665                     registryEntries.append(
 666                             "Root: HKCU; Subkey: \"Software\\Classes\\")
 667                             .append(entryName)
 668                             .append("\\DefaultIcon\"; ValueType: string; " +
 669                             "ValueName: \"\"; ValueData: \"{app}\\")
 670                             .append(icon.getName())
 671                             .append("\"\r\n");
 672                 }
 673             }
 674 
 675             if (isSystemWide) {
 676                 // "Root: HKCR;
 677                 // Subkey: \"MyProgramFile\\shell\\open\\command\";
 678                 // ValueType: string;
 679                 // ValueName: \"\";
 680                 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
 681                 registryEntries.append("Root: HKCR; Subkey: \"")
 682                         .append(entryName)
 683                         .append("\\shell\\open\\command\"; ValueType: " +
 684                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 685                         .append(APP_NAME.fetchFrom(params))
 686                         .append("\"\" \"\"%1\"\"\"\r\n");
 687             } else {
 688                 registryEntries.append(
 689                         "Root: HKCU; Subkey: \"Software\\Classes\\")
 690                         .append(entryName)
 691                         .append("\\shell\\open\\command\"; ValueType: " +
 692                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 693                         .append(APP_NAME.fetchFrom(params))
 694                         .append("\"\" \"\"%1\"\"\"\r\n");
 695             }
 696         }
 697         if (registryEntries.length() > 0) {
 698             data.put("FILE_ASSOCIATIONS",
 699                     "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" +
 700                     registryEntries.toString());
 701         } else {
 702             data.put("FILE_ASSOCIATIONS", "");
 703         }
 704 
 705         String iss = StandardBundlerParam.isRuntimeInstaller(params) ?
 706                 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE;
 707 
 708         try (Writer w = new BufferedWriter(new FileWriter(
 709                 getConfig_ExeProjectFile(params)))) {
 710 
 711             String content = preprocessTextResource(
 712                     getConfig_ExeProjectFile(params).getName(),
 713                     getString("resource.inno-setup-project-file"),
 714                     iss, data, VERBOSE.fetchFrom(params),
 715                     RESOURCE_DIR.fetchFrom(params));
 716             w.write(content);
 717         }
 718         return true;
 719     }
 720 
 721     private final static String removeQuotes(String s) {
 722         if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) {
 723             // special case for '"XXX"' return 'XXX' not '-XXX-'
 724             // note '"' and '""' are excluded from this special case
 725             s = s.substring(1, s.length() - 1);
 726         }
 727         // if there interior double quotes replace them with '-'
 728         return s.replaceAll("\"", "-");
 729     }
 730 
 731     private final static String DEFAULT_INNO_SETUP_ICON =
 732             "icon_inno_setup.bmp";
 733 
 734     private boolean prepareProjectConfig(Map<String, ? super Object> params)
 735             throws IOException {
 736         prepareMainProjectFile(params);
 737 
 738         // prepare installer icon
 739         File iconTarget = getConfig_SmallInnoSetupIcon(params);
 740         fetchResource(iconTarget.getName(),
 741                 getString("resource.setup-icon"),
 742                 DEFAULT_INNO_SETUP_ICON,
 743                 iconTarget,
 744                 VERBOSE.fetchFrom(params),
 745                 RESOURCE_DIR.fetchFrom(params));
 746 
 747         fetchResource(getConfig_Script(params).getName(),
 748                 getString("resource.post-install-script"),
 749                 (String) null,
 750                 getConfig_Script(params),
 751                 VERBOSE.fetchFrom(params),
 752                 RESOURCE_DIR.fetchFrom(params));
 753         return true;
 754     }
 755 
 756     private File getConfig_SmallInnoSetupIcon(
 757             Map<String, ? super Object> params) {
 758         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 759                 APP_NAME.fetchFrom(params) + "-setup-icon.bmp");
 760     }
 761 
 762     private File getConfig_ExeProjectFile(Map<String, ? super Object> params) {
 763         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 764                 APP_NAME.fetchFrom(params) + ".iss");
 765     }
 766 
 767 
 768     private File buildEXE(Map<String, ? super Object> params, File outdir)
 769              throws IOException {
 770         Log.verbose(MessageFormat.format(
 771              getString("message.outputting-to-location"),
 772              outdir.getAbsolutePath()));
 773 
 774         outdir.mkdirs();
 775 
 776         // run Inno Setup
 777         ProcessBuilder pb = new ProcessBuilder(
 778                 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params),
 779                 "/q",    // turn off inno setup output
 780                 "/o"+outdir.getAbsolutePath(),
 781                 getConfig_ExeProjectFile(params).getAbsolutePath());
 782         pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(params));
 783         IOUtils.exec(pb);
 784 
 785         Log.verbose(MessageFormat.format(
 786                 getString("message.output-location"),
 787                 outdir.getAbsolutePath()));
 788 
 789         // presume the result is the ".exe" file with the newest modified time
 790         // not the best solution, but it is the most reliable
 791         File result = null;
 792         long lastModified = 0;
 793         File[] list = outdir.listFiles();
 794         if (list != null) {
 795             for (File f : list) {
 796                 if (f.getName().endsWith(".exe") &&
 797                         f.lastModified() > lastModified) {
 798                     result = f;
 799                     lastModified = f.lastModified();
 800                 }
 801             }
 802         }
 803 
 804         return result;
 805     }
 806 
 807    public static void ensureByMutationFileIsRTF(File f) {
 808         if (f == null || !f.isFile()) return;
 809 
 810         try {
 811             boolean existingLicenseIsRTF = false;
 812 
 813             try (FileInputStream fin = new FileInputStream(f)) {
 814                 byte[] firstBits = new byte[7];
 815 
 816                 if (fin.read(firstBits) == firstBits.length) {
 817                     String header = new String(firstBits);
 818                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
 819                 }
 820             }
 821 
 822             if (!existingLicenseIsRTF) {
 823                 List<String> oldLicense = Files.readAllLines(f.toPath());
 824                 try (Writer w = Files.newBufferedWriter(
 825                         f.toPath(), Charset.forName("Windows-1252"))) {
 826                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
 827                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
 828                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
 829                             + "\\slmult1\\lang9\\fs20 ");
 830                     oldLicense.forEach(l -> {
 831                         try {
 832                             for (char c : l.toCharArray()) {
 833                                 if (c < 0x10) {
 834                                     w.write("\\'0");
 835                                     w.write(Integer.toHexString(c));
 836                                 } else if (c > 0xff) {
 837                                     w.write("\\ud");
 838                                     w.write(Integer.toString(c));
 839                                     w.write("?");
 840                                 } else if ((c < 0x20) || (c >= 0x80) ||
 841                                         (c == 0x5C) || (c == 0x7B) ||
 842                                         (c == 0x7D)) {
 843                                     w.write("\\'");
 844                                     w.write(Integer.toHexString(c));
 845                                 } else {
 846                                     w.write(c);
 847                                 }
 848                             }
 849                             if (l.length() < 1) {
 850                                 w.write("\\par");
 851                             } else {
 852                                 w.write(" ");
 853                             }
 854                             w.write("\r\n");
 855                         } catch (IOException e) {
 856                             Log.verbose(e);
 857                         }
 858                     });
 859                     w.write("}\r\n");
 860                 }
 861             }
 862         } catch (IOException e) {
 863             Log.verbose(e);
 864         }
 865     }
 866 
 867     private static String getString(String key)
 868             throws MissingResourceException {
 869         return I18N.getString(key);
 870     }
 871 }