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