package net.classicube.shared; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.net.URLClassLoader; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.logging.Level; import java.util.logging.Logger; // Code shared between Launcher and SelfUpdater. The two source files are identical. // These two classes cannot be combined into one because SelfUpdater must be able to // run without referencing the Launcher, and vice versa. public class SharedUpdaterCode { public static final String BASE_URL = "http://static.classicube.net/client/", LZMA_JAR_NAME = "lzma.jar", LAUNCHER_DIR_NAME = ".net.classicube.launcher", MAC_PATH_SUFFIX = "/Library/Application Support", LAUNCHER_NEW_JAR_NAME = "launcher.jar.new"; private static Constructor<?> constructor; private static File launcherPath, appDataPath; public static synchronized File getLauncherDir() throws IOException { if (launcherPath == null) { final File userDir = getAppDataDir(); launcherPath = new File(userDir, LAUNCHER_DIR_NAME); if (!launcherPath.exists() && !launcherPath.mkdirs()) { throw new IOException("Unable to create directory " + launcherPath); } } return launcherPath; } public static synchronized File getAppDataDir() { if (appDataPath == null) { final String home = System.getProperty("user.home", "."); final OperatingSystem os = OperatingSystem.detect(); switch (os) { case WINDOWS: final String appData = System.getenv("APPDATA"); if (appData != null) { appDataPath = new File(appData); } else { appDataPath = new File(home); } break; case MACOS: appDataPath = new File(home, MAC_PATH_SUFFIX); break; default: appDataPath = new File(home); } } return appDataPath; } public static File processDownload(final Logger logger, final File downloadedFile, final String remoteUrl, final String namePart) throws FileNotFoundException, IOException { if (logger == null) { throw new NullPointerException("logger"); } if (downloadedFile == null) { throw new NullPointerException("downloadedFile"); } if (remoteUrl == null) { throw new NullPointerException("remoteUrl"); } if (namePart == null) { throw new NullPointerException("namePart"); } final String remoteUrlLower = remoteUrl.toLowerCase(); logger.log(Level.FINE, "processDownload({0})", namePart); if (remoteUrlLower.endsWith(".pack.lzma")) { // decompress (LZMA) and then unpack (Pack200) final File newFile1 = File.createTempFile(namePart, ".decompressed.tmp"); decompressLzma(logger, downloadedFile, newFile1); downloadedFile.delete(); final File newFile2 = File.createTempFile(namePart, ".unpacked.tmp"); unpack200(newFile1, newFile2); newFile1.delete(); return newFile2; } else if (remoteUrlLower.endsWith(".lzma")) { // decompress (LZMA) final File newFile = File.createTempFile(namePart, ".decompressed.tmp"); decompressLzma(logger, downloadedFile, newFile); downloadedFile.delete(); return newFile; } else if (remoteUrlLower.endsWith(".pack")) { // unpack (Pack200) final File newFile = File.createTempFile(namePart, ".unpacked.tmp"); unpack200(downloadedFile, newFile); downloadedFile.delete(); return newFile; } else { return downloadedFile; } } static synchronized InputStream makeLzmaInputStream(final Logger logger, final InputStream stream) { if (logger == null) { throw new NullPointerException("logger"); } if (stream == null) { throw new NullPointerException("stream"); } try { if (constructor == null) { final File jarFile = new File(getLauncherDir(), LZMA_JAR_NAME); final URL[] jarUrl = new URL[]{jarFile.toURI().toURL()}; final URLClassLoader jarLoader = new URLClassLoader(jarUrl, SharedUpdaterCode.class.getClassLoader()); final Class<?> lzmaClass = Class.forName("LZMA.LzmaInputStream", true, jarLoader); constructor = lzmaClass.getDeclaredConstructor(InputStream.class); } return (InputStream) constructor.newInstance(stream); } catch (final IOException | ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { logger.log(Level.SEVERE, "Error creating LzmaInputStream", ex); throw new RuntimeException("Error creating LzmaInputStream", ex); } } private static void decompressLzma(final Logger logger, final File compressedInput, final File decompressedOutput) throws FileNotFoundException, IOException { if (logger == null) { throw new NullPointerException("logger"); } if (compressedInput == null) { throw new NullPointerException("compressedInput"); } if (decompressedOutput == null) { throw new NullPointerException("decompressedOutput"); } try (final FileInputStream fileIn = new FileInputStream(compressedInput)) { try (final BufferedInputStream bufferedIn = new BufferedInputStream(fileIn)) { try (final InputStream compressedIn = SharedUpdaterCode.makeLzmaInputStream(logger, bufferedIn)) { try (final FileOutputStream fileOut = new FileOutputStream(decompressedOutput)) { int len; final byte[] ioBuffer = new byte[64 * 1024]; while ((len = compressedIn.read(ioBuffer)) > 0) { fileOut.write(ioBuffer, 0, len); } } } } } } public static void testLzma(Logger logger) throws Exception { // Minimal LZMA stream byte[] lzmaTest = new byte[]{ 0x5d, 0x00, 0x00, 0x04, 0x00, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x05, 0x41, (byte) 0xfb, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xe0, 0x00, 0x00, 0x00 }; // Try to make an LZMA stream, to ensure that lzma.jar is downloaded and usable ByteArrayInputStream mockStream = new ByteArrayInputStream(lzmaTest); InputStream mockLzmaStream = SharedUpdaterCode.makeLzmaInputStream(logger, mockStream); mockLzmaStream.close(); } private static void unpack200(final File compressedInput, final File decompressedOutput) throws FileNotFoundException, IOException { if (compressedInput == null) { throw new NullPointerException("compressedInput"); } if (decompressedOutput == null) { throw new NullPointerException("decompressedOutput"); } try (final FileOutputStream fostream = new FileOutputStream(decompressedOutput)) { try (final JarOutputStream jostream = new JarOutputStream(fostream)) { final Pack200.Unpacker unpacker = Pack200.newUnpacker(); unpacker.unpack(compressedInput, jostream); } } } public enum OperatingSystem { NIX, SOLARIS, WINDOWS, MACOS, UNKNOWN; private final static String osName = System.getProperty("os.name").toLowerCase(); public static OperatingSystem detect() { if (osName.contains("win")) { return OperatingSystem.WINDOWS; } else if (osName.contains("mac")) { return OperatingSystem.MACOS; } else if (osName.contains("solaris") || osName.contains("sunos")) { return OperatingSystem.SOLARIS; } else if (osName.contains("linux") || osName.contains("unix")) { return OperatingSystem.NIX; } else { return OperatingSystem.UNKNOWN; } } } private SharedUpdaterCode() { } }