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