/* * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.oracle.tools.packager.mac; import com.oracle.tools.packager.AbstractBundler; import com.oracle.tools.packager.Bundler; import com.oracle.tools.packager.BundlerParamInfo; import com.oracle.tools.packager.ConfigException; import com.oracle.tools.packager.IOUtils; import com.oracle.tools.packager.Log; import com.oracle.tools.packager.RelativeFileSet; import com.oracle.tools.packager.UnsupportedPlatformException; import org.junit.After; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import static com.oracle.tools.packager.StandardBundlerParam.*; import static com.oracle.tools.packager.mac.MacAppBundler.*; import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.MAC_APP_IMAGE; import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; import static com.oracle.tools.packager.mac.MacPkgBundler.DEVELOPER_ID_INSTALLER_SIGNING_KEY; import static com.oracle.tools.packager.mac.MacPkgBundler.INSTALLER_SUFFIX; import static org.junit.Assert.*; public class MacPkgBundlerTest { static final int MIN_SIZE=0x100000; // 1MiB static File tmpBase; static File workDir; static File appResourcesDir; static File fakeMainJar; static File hdpiIcon; static String runtimeJdk; static Set appResources; static boolean retain = false; static boolean signingKeysPresent = false; static final File FAKE_CERT_ROOT = new File("build/tmp/tests/cert/").getAbsoluteFile(); @BeforeClass public static void prepareApp() { // only run on mac Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("os x")); runtimeJdk = System.getenv("PACKAGER_JDK_ROOT"); // and only if we have the correct JRE settings String jre = System.getProperty("java.home").toLowerCase(); Assume.assumeTrue(runtimeJdk != null || jre.endsWith("/contents/home/jre") || jre.endsWith("/contents/home/jre")); Log.setLogger(new Log.Logger(true)); Log.setDebug(true); retain = Boolean.parseBoolean(System.getProperty("RETAIN_PACKAGER_TESTS")); workDir = new File("build/tmp/tests", "macpkg"); hdpiIcon = new File("build/tmp/tests", "GenericAppHiDPI.icns"); appResourcesDir = new File("build/tmp/tests", "appResources"); fakeMainJar = new File(appResourcesDir, "mainApp.jar"); appResources = new HashSet<>(Arrays.asList(fakeMainJar, new File(appResourcesDir, "LICENSE"), new File(appResourcesDir, "LICENSE2") )); signingKeysPresent = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(new TreeMap<>()) != null; } @Before public void createTmpDir() throws IOException { if (retain) { tmpBase = new File("build/tmp/tests/macpkg"); } else { tmpBase = BUILD_ROOT.fetchFrom(new TreeMap<>()); } tmpBase.mkdir(); } public String createFakeCerts(Map p) { File config = new File(FAKE_CERT_ROOT, "pkg-cert.cfg"); config.getParentFile().mkdirs(); try { // create the config file holding the key config Files.write(config.toPath(), Arrays.asList("[ codesign ]", "keyUsage=critical,digitalSignature", "basicConstraints=critical,CA:false", "extendedKeyUsage=critical,codeSigning", "[ productbuild ]", "basicConstraints=critical,CA:false", "keyUsage=critical,digitalSignature", "extendedKeyUsage=critical,1.2.840.113635.100.4.13", "1.2.840.113635.100.6.1.14=critical,DER:0500")); // create the SSL keys ProcessBuilder pb = new ProcessBuilder("openssl", "req", "-newkey", "rsa:2048", "-nodes", "-out", FAKE_CERT_ROOT + "/pkg-app.csr", "-keyout", FAKE_CERT_ROOT + "/pkg-app.key", "-subj", "/CN=Developer ID Application: Insecure Test Cert/OU=JavaFX Dev/O=Oracle/C=US"); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); // first, for the app // create the cert pb = new ProcessBuilder("openssl", "x509", "-req", "-days", "10", "-in", FAKE_CERT_ROOT + "/pkg-app.csr", "-signkey", FAKE_CERT_ROOT + "/pkg-app.key", "-out", FAKE_CERT_ROOT + "/pkg-app.crt", "-extfile", FAKE_CERT_ROOT + "/pkg-cert.cfg", "-extensions", "codesign"); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); // create and add it to the keychain pb = new ProcessBuilder("certtool", "i", FAKE_CERT_ROOT + "/pkg-app.crt", "k=" + FAKE_CERT_ROOT + "/pkg.keychain", "r=" + FAKE_CERT_ROOT + "/pkg-app.key", "c", "p="); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); // create the pkg SSL keys pb = new ProcessBuilder("openssl", "req", "-newkey", "rsa:2048", "-nodes", "-out", FAKE_CERT_ROOT + "/pkg-pkg.csr", "-keyout", FAKE_CERT_ROOT + "/pkg-pkg.key", "-subj", "/CN=Developer ID Installer: Insecure Test Cert/OU=JavaFX Dev/O=Oracle/C=US"); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); // create the pkg cert pb = new ProcessBuilder("openssl", "x509", "-req", "-days", "10", "-in", FAKE_CERT_ROOT + "/pkg-pkg.csr", "-signkey", FAKE_CERT_ROOT + "/pkg-pkg.key", "-out", FAKE_CERT_ROOT + "/pkg-pkg.crt", "-extfile",FAKE_CERT_ROOT + "/pkg-cert.cfg", "-extensions", "productbuild"); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); // create and add it to the keychain pb = new ProcessBuilder("certtool", "i", FAKE_CERT_ROOT + "/pkg-pkg.crt", "k=" + FAKE_CERT_ROOT + "/pkg.keychain", "r=" + FAKE_CERT_ROOT + "/pkg-pkg.key"); IOUtils.exec(pb, VERBOSE.fetchFrom(p)); return FAKE_CERT_ROOT + "/pkg.keychain"; } catch (IOException e) { e.printStackTrace(); } return null; } @After public void maybeCleanupTmpDir() { if (!retain) { attemptDelete(tmpBase); } attemptDelete(FAKE_CERT_ROOT); } private void attemptDelete(File tmpBase) { if (tmpBase.isDirectory()) { File[] children = tmpBase.listFiles(); if (children != null) { for (File f : children) { attemptDelete(f); } } } boolean success; try { success = !tmpBase.exists() || tmpBase.delete(); } catch (SecurityException se) { success = false; } if (!success) { System.err.println("Could not clean up " + tmpBase.toString()); } } /** * See if smoke comes out */ @Test public void smokeTest() throws IOException, ConfigException, UnsupportedPlatformException { AbstractBundler bundler = new MacPkgBundler(); assertNotNull(bundler.getName()); assertNotNull(bundler.getID()); assertNotNull(bundler.getDescription()); //assertNotNull(bundler.getBundleParameters()); Map bundleParams = new HashMap<>(); bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(APP_NAME.getID(), "Smoke Test"); bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle"); bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id"); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); bundleParams.put(MAIN_JAR.getID(), new RelativeFileSet(fakeMainJar.getParentFile(), new HashSet<>(Arrays.asList(fakeMainJar))) ); bundleParams.put(CLASSPATH.getID(), "mainApp.jar"); bundleParams.put(VERBOSE.getID(), true); bundleParams.put(LICENSE_FILE.getID(), Arrays.asList("LICENSE", "LICENSE2")); bundleParams.put(SIGN_BUNDLE.getID(), false); // force no signing if (runtimeJdk != null) { //FIXME bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } boolean valid = bundler.validate(bundleParams); assertTrue(valid); File result = bundler.execute(bundleParams, new File(workDir, "smoke")); System.err.println("Bundle at - " + result); assertNotNull(result); assertTrue(result.exists()); assertTrue(result.length() > MIN_SIZE); } /** * Build smoke test and mark it as quarantined, possibly signed */ @Test public void quarantinedAppTest() throws IOException, ConfigException, UnsupportedPlatformException { AbstractBundler bundler = new MacPkgBundler(); assertNotNull(bundler.getName()); assertNotNull(bundler.getID()); assertNotNull(bundler.getDescription()); //assertNotNull(bundler.getBundleParameters()); Map bundleParams = new HashMap<>(); bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(APP_NAME.getID(), "Quarantine App"); bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle"); bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id"); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); bundleParams.put(VERBOSE.getID(), true); if (runtimeJdk != null) { //FIXME bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } if (!signingKeysPresent) { String keychain = createFakeCerts(bundleParams); Assume.assumeNotNull(keychain); bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain); } boolean valid = bundler.validate(bundleParams); assertTrue(valid); File result = bundler.execute(bundleParams, new File(workDir, "quarantine")); System.err.println("Bundle at - " + result); assertNotNull(result); assertTrue(result.exists()); assertTrue(result.length() > MIN_SIZE); validateSignatures(result); // mark it as though it's been downloaded ProcessBuilder pb = new ProcessBuilder( "xattr", "-w", "com.apple.quarantine", "0000;" + Long.toHexString(System.currentTimeMillis() / 1000L) + ";Java Unit Tests;|com.oracle.jvm.8u", result.toString()); IOUtils.exec(pb, true); } /** * The bare minimum configuration needed to make it work *
    *
  • Where to build it
  • *
  • The jar containing the application (with a main-class attribute)
  • *
* * All other values will be driven off of those two values. */ @Test public void minimumConfig() throws IOException, ConfigException, UnsupportedPlatformException { Bundler bundler = new MacPkgBundler(); Map bundleParams = new HashMap<>(); bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); if (runtimeJdk != null) { //FIXME bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } String keychain = null; if (!signingKeysPresent) { keychain = createFakeCerts(bundleParams); if (keychain != null) { bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain); } } boolean valid = bundler.validate(bundleParams); assertTrue(valid); File output = bundler.execute(bundleParams, new File(workDir, "BareMinimum")); System.err.println("Bundle at - " + output); assertNotNull(output); assertTrue(output.exists()); assertTrue(output.length() > MIN_SIZE); if (signingKeysPresent || keychain != null) { validateSignatures(output); } } /** * Test with unicode in places we expect it to be */ @Test public void unicodeConfig() throws IOException, ConfigException, UnsupportedPlatformException { Bundler bundler = new MacPkgBundler(); Map bundleParams = new HashMap<>(); bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); bundleParams.put(APP_NAME.getID(), "хелловорлд"); bundleParams.put(TITLE.getID(), "ХеллоВорлд аппликейшн"); bundleParams.put(VENDOR.getID(), "Оракл девелопмент"); bundleParams.put(DESCRIPTION.getID(), "крайне большое описание со странными символами"); if (runtimeJdk != null) { //FIXME bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } String keychain = null; if (!signingKeysPresent) { keychain = createFakeCerts(bundleParams); if (keychain != null) { bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain); } } bundler.validate(bundleParams); File output = bundler.execute(bundleParams, new File(workDir, "Unicode")); System.err.println("Bundle at - " + output); assertNotNull(output); assertTrue(output.exists()); if (signingKeysPresent || keychain != null) { validateSignatures(output); } } /** * Create a PKG with an external app rather than a self-created one. */ @Test public void externalApp() throws IOException, ConfigException, UnsupportedPlatformException { // only run with full tests Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST"))); // first create the external app Bundler appBundler = new MacAppBundler(); Map appBundleParams = new HashMap<>(); appBundleParams.put(BUILD_ROOT.getID(), tmpBase); appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); appBundleParams.put(APP_NAME.getID(), "External APP PKG Test"); appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external"); appBundleParams.put(VERBOSE.getID(), true); if (runtimeJdk != null) { //FIXME appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } String keychain = null; if (!signingKeysPresent) { keychain = createFakeCerts(appBundleParams); if (keychain != null) { appBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain); } } boolean valid = appBundler.validate(appBundleParams); assertTrue(valid); File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalApp1")); System.err.println("App at - " + appOutput); assertNotNull(appOutput); assertTrue(appOutput.exists()); // now create the PKG referencing this external app Bundler pkgBundler = new MacPkgBundler(); Map pkgBundleParams = new HashMap<>(); pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase); pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput); pkgBundleParams.put(APP_NAME.getID(), "External APP PKG Test"); pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external"); pkgBundleParams.put(VERBOSE.getID(), true); if (runtimeJdk != null) { //FIXME pkgBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } if (keychain != null) { pkgBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain); } valid = pkgBundler.validate(pkgBundleParams); assertTrue(valid); File pkgOutput = pkgBundler.execute(pkgBundleParams, new File(workDir, "PKGExternalApp2")); System.err.println(".pkg at - " + pkgOutput); assertNotNull(pkgOutput); assertTrue(pkgOutput.exists()); assertTrue(pkgOutput.length() > MIN_SIZE); if (signingKeysPresent || keychain != null) { validateSignatures(pkgOutput); } } @Test(expected = ConfigException.class) public void externanNoAppName() throws ConfigException, UnsupportedPlatformException { Bundler pkgBundler = new MacPkgBundler(); Map pkgBundleParams = new HashMap<>(); pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase); pkgBundleParams.put(MAC_APP_IMAGE.getID(), "."); pkgBundleParams.put(IDENTIFIER.getID(), "net.example.bogus"); pkgBundleParams.put(VERBOSE.getID(), true); pkgBundler.validate(pkgBundleParams); } @Test(expected = ConfigException.class) public void externanNoID() throws ConfigException, UnsupportedPlatformException { Bundler pkgBundler = new MacPkgBundler(); Map pkgBundleParams = new HashMap<>(); pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase); pkgBundleParams.put(MAC_APP_IMAGE.getID(), "."); pkgBundleParams.put(APP_NAME.getID(), "Bogus App"); pkgBundleParams.put(VERBOSE.getID(), true); pkgBundler.validate(pkgBundleParams); } @Test(expected = ConfigException.class) public void invalidLicenseFile() throws ConfigException, UnsupportedPlatformException { Bundler bundler = new MacPkgBundler(); Map bundleParams = new HashMap<>(); bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); bundleParams.put(LICENSE_FILE.getID(), "BOGUS_LICENSE"); bundler.validate(bundleParams); } /** * Test a misconfiguration where signature is requested but no key is specified. */ @Test public void signButNoCert() throws IOException, ConfigException, UnsupportedPlatformException { // only run with full tests Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST"))); try { // first create the external app Bundler appBundler = new MacAppBundler(); Map appBundleParams = new HashMap<>(); appBundleParams.put(BUILD_ROOT.getID(), tmpBase); appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); appBundleParams.put(APP_NAME.getID(), "External APP PKG Negative Signature Test"); appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external"); appBundleParams.put(VERBOSE.getID(), true); if (runtimeJdk != null) { //FIXME appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk); } boolean valid = appBundler.validate(appBundleParams); assertTrue(valid); File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalAppSignTest")); System.err.println("App at - " + appOutput); assertNotNull(appOutput); assertTrue(appOutput.exists()); // now create the PKG referencing this external app Bundler pkgBundler = new MacPkgBundler(); Map pkgBundleParams = new HashMap<>(); pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase); pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput); pkgBundleParams.put(APP_NAME.getID(), "Negative Signature Test"); pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external"); pkgBundleParams.put(SIGN_BUNDLE.getID(), true); pkgBundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), null); pkgBundler.validate(pkgBundleParams); // if we get here we fail assertTrue("ConfigException should have been thrown", false); } catch (ConfigException ignore) { // expected } } @Test public void configureEverything() throws Exception { AbstractBundler bundler = new MacPkgBundler(); Collection> parameters = bundler.getBundleParameters(); Map bundleParams = new HashMap<>(); bundleParams.put(APP_NAME.getID(), "Everything App Name"); bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources)); bundleParams.put(ARGUMENTS.getID(), Arrays.asList("He Said", "She Said")); bundleParams.put(BUNDLE_ID_SIGNING_PREFIX.getID(), "everything.signing.prefix."); bundleParams.put(CLASSPATH.getID(), "mainApp.jar"); bundleParams.put(ICON_ICNS.getID(), hdpiIcon); bundleParams.put(INSTALLER_SUFFIX.getID(), "-PKG-TEST"); bundleParams.put(JVM_OPTIONS.getID(), "-Xms128M"); bundleParams.put(JVM_PROPERTIES.getID(), "everything.jvm.property=everything.jvm.property.value"); bundleParams.put(MAC_CATEGORY.getID(), "public.app-category.developer-tools"); bundleParams.put(MAC_CF_BUNDLE_IDENTIFIER.getID(), "com.example.everything.cf-bundle-identifier"); bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Everything CF Bundle Name"); //FIXME bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk == null ? System.getProperty("java.home") : runtimeJdk); bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle"); bundleParams.put(MAIN_JAR.getID(), "mainApp.jar"); bundleParams.put(PREFERENCES_ID.getID(), "everything/preferences/id"); bundleParams.put(PRELOADER_CLASS.getID(), "hello.HelloPreloader"); bundleParams.put(SIGNING_KEYCHAIN.getID(), ""); bundleParams.put(USER_JVM_OPTIONS.getID(), "-Xmx=256M\n"); bundleParams.put(VERSION.getID(), "1.2.3.4"); //bundleParams.put(IDENTIFIER.getID(), "com.example.everything.identifier"); bundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), "Developer ID Installer"); bundleParams.put(LICENSE_FILE.getID(), "LICENSE"); //bundleParams.put(SERVICE_HINT.getID(), false); // assert they are set for (BundlerParamInfo bi :parameters) { assertNotNull("Bundle args Contains " + bi.getID(), bundleParams.containsKey(bi.getID())); } // and only those are set bundleParamLoop: for (String s :bundleParams.keySet()) { for (BundlerParamInfo bpi : parameters) { if (s.equals(bpi.getID())) { continue bundleParamLoop; } } fail("Enumerated parameters does not contain " + s); } // assert they resolve for (BundlerParamInfo bi :parameters) { bi.fetchFrom(bundleParams); } // add verbose now that we are done scoping out parameters bundleParams.put(BUILD_ROOT.getID(), tmpBase); bundleParams.put(VERBOSE.getID(), true); // assert it validates boolean valid = bundler.validate(bundleParams); assertTrue(valid); // only run the bundle with full tests Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST"))); // but first remove signing keys, test servers don't have these... bundleParams.remove(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID()); File result = bundler.execute(bundleParams, new File(workDir, "everything")); System.err.println("Bundle at - " + result); assertNotNull(result); assertTrue(result.exists()); assertTrue(result.length() > MIN_SIZE); } public void validateSignatures(File appLocation) throws IOException { // Check the signatures with pkgUtil ProcessBuilder pb = new ProcessBuilder( "pkgutil", "--check-signature", appLocation.getCanonicalPath()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); try { IOUtils.exec(pb, true, false, ps); } catch (IOException ioe) { if (signingKeysPresent) { // these were real keys, failures are real throw ioe; } // failure was for bogus key if (ioe.getMessage().contains("Exec failed with code 1 ")) { // this is likely because the key is not signed by apple, lets look // ok, look to see if our key is in the output if (!baos.toString().contains("1. Developer ID Installer: Insecure Test Cert")) { // didn't list our key as #1, must be some other error throw ioe; } // ok, this is expected. Ignore it. } else { // some other failure, throw the error. throw ioe; } } } }