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> p,
 173             File outputParentDir) throws PackagerException {
 174         return bundle(p, 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, Log.isDebug(), 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> p)
 207             throws UnsupportedPlatformException, ConfigException {
 208         try {
 209             if (p == 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(p).validate(p);
 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(p);
 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(p).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(p));
 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(p);
 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> p)
 288                 throws PackagerException, IOException {
 289         File appImage = StandardBundlerParam.getPredefinedAppImage(p);
 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(
 295                     EXE_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p));
 296             // copy everything from appImage dir into appDir/name
 297             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
 298         } else {
 299             appDir = APP_BUNDLER.fetchFrom(p).doBundle(p,
 300                     EXE_IMAGE_DIR.fetchFrom(p), true);
 301         }
 302 
 303         if (appDir == null) {
 304             return false;
 305         }
 306 
 307         p.put(WIN_APP_IMAGE.getID(), appDir);
 308 
 309         String licenseFile = LICENSE_FILE.fetchFrom(p);
 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(p), lfile.getName());
 314             IOUtils.copyFile(lfile, destFile);
 315             ensureByMutationFileIsRTF(destFile);
 316         }
 317 
 318         // copy file association icons
 319         List<Map<String, ? super Object>> fileAssociations =
 320                 FILE_ASSOCIATIONS.fetchFrom(p);
 321 
 322         for (Map<String, ? super Object> fa : fileAssociations) {
 323             File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
 324             if (icon == null) {
 325                 continue;
 326             }
 327 
 328             File faIconFile = new File(appDir, icon.getName());
 329 
 330             if (icon.exists()) {
 331                 try {
 332                     IOUtils.copyFile(icon, faIconFile);
 333                 } catch (IOException e) {
 334                     Log.verbose(e);
 335                 }
 336             }
 337         }
 338 
 339         return true;
 340     }
 341 
 342     public File bundle(Map<String, ? super Object> p, File outdir)
 343             throws PackagerException {
 344         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 345             throw new PackagerException("error.cannot-create-output-dir",
 346                     outdir.getAbsolutePath());
 347         }
 348         if (!outdir.canWrite()) {
 349             throw new PackagerException("error.cannot-write-to-output-dir",
 350                     outdir.getAbsolutePath());
 351         }
 352 
 353         String tempDirectory = WindowsDefender.getUserTempDirectory();
 354         if (Arguments.CLIOptions.context().userProvidedBuildRoot) {
 355             tempDirectory = TEMP_ROOT.fetchFrom(p).getAbsolutePath();
 356         }
 357         if (WindowsDefender.isThereAPotentialWindowsDefenderIssue(
 358                 tempDirectory)) {
 359             Log.error(MessageFormat.format(
 360                     getString("message.potential.windows.defender.issue"),
 361                     tempDirectory));
 362         }
 363 
 364         // validate we have valid tools before continuing
 365         String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p);
 366         if (iscc == null || !new File(iscc).isFile()) {
 367             Log.verbose(getString("error.iscc-not-found"));
 368             Log.verbose(MessageFormat.format(
 369                     getString("message.iscc-file-string"), iscc));
 370             throw new PackagerException("error.iscc-not-found");
 371         }
 372 
 373         File imageDir = EXE_IMAGE_DIR.fetchFrom(p);
 374         try {
 375             imageDir.mkdirs();
 376 
 377             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 378             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 379             if (!menuShortcut && !desktopShortcut) {
 380                 // both can not be false - user will not find the app
 381                 Log.verbose(getString("message.one-shortcut-required"));
 382                 p.put(MENU_HINT.getID(), true);
 383             }
 384 
 385             if (prepareProto(p) && prepareProjectConfig(p)) {
 386                 File configScript = getConfig_Script(p);
 387                 if (configScript.exists()) {
 388                     Log.verbose(MessageFormat.format(
 389                             getString("message.running-wsh-script"),
 390                             configScript.getAbsolutePath()));
 391                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
 392                 }
 393                 return buildEXE(p, outdir);
 394             }
 395             return null;
 396         } catch (IOException ex) {
 397             Log.verbose(ex);
 398             throw new PackagerException(ex);
 399         }
 400     }
 401 
 402     // name of post-image script
 403     private File getConfig_Script(Map<String, ? super Object> p) {
 404         return new File(EXE_IMAGE_DIR.fetchFrom(p),
 405                 APP_NAME.fetchFrom(p) + "-post-image.wsf");
 406     }
 407 
 408     private String getAppIdentifier(Map<String, ? super Object> p) {
 409         String nm = UPGRADE_UUID.fetchFrom(p).toString();
 410 
 411         // limitation of innosetup
 412         if (nm.length() > 126) {
 413             Log.error(getString("message-truncating-id"));
 414             nm = nm.substring(0, 126);
 415         }
 416 
 417         return nm;
 418     }
 419 
 420     private String getLicenseFile(Map<String, ? super Object> p) {
 421         String licenseFile = LICENSE_FILE.fetchFrom(p);
 422         if (licenseFile != null) {
 423             File lfile = new File(licenseFile);
 424             File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName());
 425             String filePath = destFile.getAbsolutePath();
 426             if (filePath.contains(" ")) {
 427                 return "\"" + filePath + "\"";
 428             } else {
 429                 return filePath;
 430             }
 431         }
 432 
 433         return null;
 434     }
 435 
 436     void validateValueAndPut(Map<String, String> data, String key,
 437                 BundlerParamInfo<String> param,
 438                 Map<String, ? super Object> p) throws IOException {
 439         String value = param.fetchFrom(p);
 440         if (value.contains("\r") || value.contains("\n")) {
 441             throw new IOException("Configuration Parameter " +
 442                      param.getID() + " cannot contain multiple lines of text");
 443         }
 444         data.put(key, innosetupEscape(value));
 445     }
 446 
 447     private String innosetupEscape(String value) {
 448         if (value == null) {
 449             return "";
 450         }
 451 
 452         if (value.contains("\"") || !value.trim().equals(value)) {
 453             value = "\"" + value.replace("\"", "\"\"") + "\"";
 454         }
 455         return value;
 456     }
 457 
 458     boolean prepareMainProjectFile(Map<String, ? super Object> p)
 459             throws IOException {
 460         Map<String, String> data = new HashMap<>();
 461         data.put("PRODUCT_APP_IDENTIFIER",
 462                 innosetupEscape(getAppIdentifier(p)));
 463 
 464         validateValueAndPut(data, "INSTALL_DIR", WINDOWS_INSTALL_DIR, p);
 465         validateValueAndPut(data, "INSTALLER_NAME", APP_NAME, p);
 466         validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, p);
 467         validateValueAndPut(data, "APPLICATION_VERSION", VERSION, p);
 468         validateValueAndPut(data, "INSTALLER_FILE_NAME",
 469                 INSTALLER_FILE_NAME, p);
 470 
 471         data.put("LAUNCHER_NAME",
 472                 innosetupEscape(WinAppBundler.getAppName(p)));
 473 
 474         data.put("APPLICATION_LAUNCHER_FILENAME",
 475                 innosetupEscape(WinAppBundler.getLauncherName(p)));
 476 
 477         data.put("APPLICATION_DESKTOP_SHORTCUT",
 478                 SHORTCUT_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
 479         data.put("APPLICATION_MENU_SHORTCUT",
 480                 MENU_HINT.fetchFrom(p) ? "returnTrue" : "returnFalse");
 481         validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, p);
 482         validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, p);
 483 
 484         data.put("APPLICATION_LICENSE_FILE",
 485                 innosetupEscape(getLicenseFile(p)));
 486         data.put("DISABLE_DIR_PAGE",
 487                 INSTALLDIR_CHOOSER.fetchFrom(p) ? "No" : "Yes");
 488 
 489         Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(p);
 490 
 491         if (isSystemWide) {
 492             data.put("APPLICATION_INSTALL_ROOT", "{pf}");
 493             data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
 494         } else {
 495             data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
 496             data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
 497         }
 498 
 499         data.put("ARCHITECTURE_BIT_MODE", "x64");
 500 
 501         validateValueAndPut(data, "RUN_FILENAME", APP_NAME, p);
 502 
 503         validateValueAndPut(data, "APPLICATION_DESCRIPTION",
 504                 DESCRIPTION, p);
 505 
 506         data.put("APPLICATION_SERVICE", "returnFalse");
 507         data.put("APPLICATION_NOT_SERVICE", "returnFalse");
 508         data.put("APPLICATION_APP_CDS_INSTALL", "returnFalse");
 509         data.put("START_ON_INSTALL", "");
 510         data.put("STOP_ON_UNINSTALL", "");
 511         data.put("RUN_AT_STARTUP", "");
 512 
 513         String imagePathString =
 514                 WIN_APP_IMAGE.fetchFrom(p).toPath().toAbsolutePath().toString();
 515         data.put("APPLICATION_IMAGE", innosetupEscape(imagePathString));
 516         Log.verbose("setting APPLICATION_IMAGE to " +
 517                 innosetupEscape(imagePathString) + " for InnoSetup");
 518 
 519         StringBuilder addLaunchersCfg = new StringBuilder();
 520         for (Map<String, ? super Object>
 521                 launcher : ADD_LAUNCHERS.fetchFrom(p)) {
 522             String application_name = APP_NAME.fetchFrom(launcher);
 523             if (MENU_HINT.fetchFrom(launcher)) {
 524                 // Name: "{group}\APPLICATION_NAME";
 525                 // Filename: "{app}\APPLICATION_NAME.exe";
 526                 // IconFilename: "{app}\APPLICATION_NAME.ico"
 527                 addLaunchersCfg.append("Name: \"{group}\\");
 528                 addLaunchersCfg.append(application_name);
 529                 addLaunchersCfg.append("\"; Filename: \"{app}\\");
 530                 addLaunchersCfg.append(application_name);
 531                 addLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
 532                 addLaunchersCfg.append(application_name);
 533                 addLaunchersCfg.append(".ico\"\r\n");
 534             }
 535             if (SHORTCUT_HINT.fetchFrom(launcher)) {
 536                 // Name: "{commondesktop}\APPLICATION_NAME";
 537                 // Filename: "{app}\APPLICATION_NAME.exe";
 538                 // IconFilename: "{app}\APPLICATION_NAME.ico"
 539                 addLaunchersCfg.append("Name: \"{commondesktop}\\");
 540                 addLaunchersCfg.append(application_name);
 541                 addLaunchersCfg.append("\"; Filename: \"{app}\\");
 542                 addLaunchersCfg.append(application_name);
 543                 addLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
 544                 addLaunchersCfg.append(application_name);
 545                 addLaunchersCfg.append(".ico\"\r\n");
 546             }
 547         }
 548         data.put("ADD_LAUNCHERS", addLaunchersCfg.toString());
 549 
 550         StringBuilder registryEntries = new StringBuilder();
 551         String regName = APP_REGISTRY_NAME.fetchFrom(p);
 552         List<Map<String, ? super Object>> fetchFrom =
 553                 FILE_ASSOCIATIONS.fetchFrom(p);
 554         for (int i = 0; i < fetchFrom.size(); i++) {
 555             Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
 556             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 557             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 558 
 559             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 560             String entryName = regName + "File";
 561             if (i > 0) {
 562                 entryName += "." + i;
 563             }
 564 
 565             if (extensions == null) {
 566                 Log.verbose(getString(
 567                         "message.creating-association-with-null-extension"));
 568             } else {
 569                 for (String ext : extensions) {
 570                     if (isSystemWide) {
 571                         // "Root: HKCR; Subkey: \".myp\";
 572                         // ValueType: string; ValueName: \"\";
 573                         // ValueData: \"MyProgramFile\";
 574                         // Flags: uninsdeletevalue"
 575                         registryEntries.append("Root: HKCR; Subkey: \".")
 576                                 .append(ext)
 577                                 .append("\"; ValueType: string;"
 578                                 + " ValueName: \"\"; ValueData: \"")
 579                                 .append(entryName)
 580                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 581                     } else {
 582                         registryEntries.append(
 583                                 "Root: HKCU; Subkey: \"Software\\Classes\\.")
 584                                 .append(ext)
 585                                 .append("\"; ValueType: string;"
 586                                 + " ValueName: \"\"; ValueData: \"")
 587                                 .append(entryName)
 588                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 589                     }
 590                 }
 591             }
 592 
 593             if (extensions != null && !extensions.isEmpty()) {
 594                 String ext = extensions.get(0);
 595                 List<String> mimeTypes =
 596                         FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 597                 for (String mime : mimeTypes) {
 598                     if (isSystemWide) {
 599                         // "Root: HKCR;
 600                         // Subkey: HKCR\\Mime\\Database\\
 601                         //         Content Type\\application/chaos;
 602                         // ValueType: string;
 603                         // ValueName: Extension;
 604                         // ValueData: .chaos;
 605                         // Flags: uninsdeletevalue"
 606                         registryEntries.append("Root: HKCR; Subkey: " +
 607                                  "\"Mime\\Database\\Content Type\\")
 608                             .append(mime)
 609                             .append("\"; ValueType: string; ValueName: " +
 610                                  "\"Extension\"; ValueData: \".")
 611                             .append(ext)
 612                             .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 613                     } else {
 614                         registryEntries.append(
 615                                 "Root: HKCU; Subkey: \"Software\\" +
 616                                 "Classes\\Mime\\Database\\Content Type\\")
 617                                 .append(mime)
 618                                 .append("\"; ValueType: string; " +
 619                                 "ValueName: \"Extension\"; ValueData: \".")
 620                                 .append(ext)
 621                                 .append("\"; Flags: uninsdeletevalue uninsdeletekeyifempty\r\n");
 622                     }
 623                 }
 624             }
 625 
 626             if (isSystemWide) {
 627                 // "Root: HKCR;
 628                 // Subkey: \"MyProgramFile\";
 629                 // ValueType: string;
 630                 // ValueName: \"\";
 631                 // ValueData: \"My Program File\";
 632                 // Flags: uninsdeletekey"
 633                 registryEntries.append("Root: HKCR; Subkey: \"")
 634                     .append(entryName)
 635                     .append(
 636                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 637                     .append(removeQuotes(description))
 638                     .append("\"; Flags: uninsdeletekey\r\n");
 639             } else {
 640                 registryEntries.append(
 641                     "Root: HKCU; Subkey: \"Software\\Classes\\")
 642                     .append(entryName)
 643                     .append(
 644                     "\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 645                     .append(removeQuotes(description))
 646                     .append("\"; Flags: uninsdeletekey\r\n");
 647             }
 648 
 649             if (icon != null && icon.exists()) {
 650                 if (isSystemWide) {
 651                     // "Root: HKCR;
 652                     // Subkey: \"MyProgramFile\\DefaultIcon\";
 653                     // ValueType: string;
 654                     // ValueName: \"\";
 655                     // ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
 656                     registryEntries.append("Root: HKCR; Subkey: \"")
 657                             .append(entryName)
 658                             .append("\\DefaultIcon\"; ValueType: string; " +
 659                             "ValueName: \"\"; ValueData: \"{app}\\")
 660                             .append(icon.getName())
 661                             .append("\"\r\n");
 662                 } else {
 663                     registryEntries.append(
 664                             "Root: HKCU; Subkey: \"Software\\Classes\\")
 665                             .append(entryName)
 666                             .append("\\DefaultIcon\"; ValueType: string; " +
 667                             "ValueName: \"\"; ValueData: \"{app}\\")
 668                             .append(icon.getName())
 669                             .append("\"\r\n");
 670                 }
 671             }
 672 
 673             if (isSystemWide) {
 674                 // "Root: HKCR;
 675                 // Subkey: \"MyProgramFile\\shell\\open\\command\";
 676                 // ValueType: string;
 677                 // ValueName: \"\";
 678                 // ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
 679                 registryEntries.append("Root: HKCR; Subkey: \"")
 680                         .append(entryName)
 681                         .append("\\shell\\open\\command\"; ValueType: " +
 682                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 683                         .append(APP_NAME.fetchFrom(p))
 684                         .append("\"\" \"\"%1\"\"\"\r\n");
 685             } else {
 686                 registryEntries.append(
 687                         "Root: HKCU; Subkey: \"Software\\Classes\\")
 688                         .append(entryName)
 689                         .append("\\shell\\open\\command\"; ValueType: " +
 690                         "string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 691                         .append(APP_NAME.fetchFrom(p))
 692                         .append("\"\" \"\"%1\"\"\"\r\n");
 693             }
 694         }
 695         if (registryEntries.length() > 0) {
 696             data.put("FILE_ASSOCIATIONS",
 697                     "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" +
 698                     registryEntries.toString());
 699         } else {
 700             data.put("FILE_ASSOCIATIONS", "");
 701         }
 702 
 703         String iss = StandardBundlerParam.isRuntimeInstaller(p) ?
 704                 DEFAULT_JRE_EXE_TEMPLATE : DEFAULT_EXE_PROJECT_TEMPLATE;
 705 
 706         Writer w = new BufferedWriter(new FileWriter(
 707                 getConfig_ExeProjectFile(p)));
 708 
 709         String content = preprocessTextResource(
 710                 getConfig_ExeProjectFile(p).getName(),
 711                 getString("resource.inno-setup-project-file"),
 712                 iss, data, VERBOSE.fetchFrom(p),
 713                 RESOURCE_DIR.fetchFrom(p));
 714         w.write(content);
 715         w.close();
 716         return true;
 717     }
 718 
 719     private final static String removeQuotes(String s) {
 720         if (s.length() > 2 && s.startsWith("\"") && s.endsWith("\"")) {
 721             // special case for '"XXX"' return 'XXX' not '-XXX-'
 722             // note '"' and '""' are excluded from this special case
 723             s = s.substring(1, s.length() - 1);
 724         }
 725         // if there interior double quotes replace them with '-'
 726         return s.replaceAll("\"", "-");
 727     }
 728 
 729     private final static String DEFAULT_INNO_SETUP_ICON =
 730             "icon_inno_setup.bmp";
 731 
 732     private boolean prepareProjectConfig(Map<String, ? super Object> p)
 733             throws IOException {
 734         prepareMainProjectFile(p);
 735 
 736         // prepare installer icon
 737         File iconTarget = getConfig_SmallInnoSetupIcon(p);
 738         fetchResource(iconTarget.getName(),
 739                 getString("resource.setup-icon"),
 740                 DEFAULT_INNO_SETUP_ICON,
 741                 iconTarget,
 742                 VERBOSE.fetchFrom(p),
 743                 RESOURCE_DIR.fetchFrom(p));
 744 
 745         fetchResource(getConfig_Script(p).getName(),
 746                 getString("resource.post-install-script"),
 747                 (String) null,
 748                 getConfig_Script(p),
 749                 VERBOSE.fetchFrom(p),
 750                 RESOURCE_DIR.fetchFrom(p));
 751         return true;
 752     }
 753 
 754     private File getConfig_SmallInnoSetupIcon(
 755             Map<String, ? super Object> p) {
 756         return new File(EXE_IMAGE_DIR.fetchFrom(p),
 757                 APP_NAME.fetchFrom(p) + "-setup-icon.bmp");
 758     }
 759 
 760     private File getConfig_ExeProjectFile(Map<String, ? super Object> p) {
 761         return new File(EXE_IMAGE_DIR.fetchFrom(p),
 762                 APP_NAME.fetchFrom(p) + ".iss");
 763     }
 764 
 765 
 766     private File buildEXE(Map<String, ? super Object> p, File outdir)
 767              throws IOException {
 768         Log.verbose(MessageFormat.format(
 769              getString("message.outputting-to-location"),
 770              outdir.getAbsolutePath()));
 771 
 772         outdir.mkdirs();
 773 
 774         // run Inno Setup
 775         ProcessBuilder pb = new ProcessBuilder(
 776                 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p),
 777                 "/q",    // turn off inno setup output
 778                 "/o"+outdir.getAbsolutePath(),
 779                 getConfig_ExeProjectFile(p).getAbsolutePath());
 780         pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(p));
 781         IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 782 
 783         Log.verbose(MessageFormat.format(
 784                 getString("message.output-location"),
 785                 outdir.getAbsolutePath()));
 786 
 787         // presume the result is the ".exe" file with the newest modified time
 788         // not the best solution, but it is the most reliable
 789         File result = null;
 790         long lastModified = 0;
 791         File[] list = outdir.listFiles();
 792         if (list != null) {
 793             for (File f : list) {
 794                 if (f.getName().endsWith(".exe") &&
 795                         f.lastModified() > lastModified) {
 796                     result = f;
 797                     lastModified = f.lastModified();
 798                 }
 799             }
 800         }
 801 
 802         return result;
 803     }
 804 
 805    public static void ensureByMutationFileIsRTF(File f) {
 806         if (f == null || !f.isFile()) return;
 807 
 808         try {
 809             boolean existingLicenseIsRTF = false;
 810 
 811             try (FileInputStream fin = new FileInputStream(f)) {
 812                 byte[] firstBits = new byte[7];
 813 
 814                 if (fin.read(firstBits) == firstBits.length) {
 815                     String header = new String(firstBits);
 816                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
 817                 }
 818             }
 819 
 820             if (!existingLicenseIsRTF) {
 821                 List<String> oldLicense = Files.readAllLines(f.toPath());
 822                 try (Writer w = Files.newBufferedWriter(
 823                         f.toPath(), Charset.forName("Windows-1252"))) {
 824                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
 825                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
 826                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
 827                             + "\\slmult1\\lang9\\fs20 ");
 828                     oldLicense.forEach(l -> {
 829                         try {
 830                             for (char c : l.toCharArray()) {
 831                                 if (c < 0x10) {
 832                                     w.write("\\'0");
 833                                     w.write(Integer.toHexString(c));
 834                                 } else if (c > 0xff) {
 835                                     w.write("\\ud");
 836                                     w.write(Integer.toString(c));
 837                                     w.write("?");
 838                                 } else if ((c < 0x20) || (c >= 0x80) ||
 839                                         (c == 0x5C) || (c == 0x7B) ||
 840                                         (c == 0x7D)) {
 841                                     w.write("\\'");
 842                                     w.write(Integer.toHexString(c));
 843                                 } else {
 844                                     w.write(c);
 845                                 }
 846                             }
 847                             if (l.length() < 1) {
 848                                 w.write("\\par");
 849                             } else {
 850                                 w.write(" ");
 851                             }
 852                             w.write("\r\n");
 853                         } catch (IOException e) {
 854                             Log.verbose(e);
 855                         }
 856                     });
 857                     w.write("}\r\n");
 858                 }
 859             }
 860         } catch (IOException e) {
 861             Log.verbose(e);
 862         }
 863     }
 864 
 865     private static String getString(String key)
 866             throws MissingResourceException {
 867         return I18N.getString(key);
 868     }
 869 }