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