/* * This file is a part of Alchemy OS project. * Copyright (C) 2011-2014, Sergey Basalaev <sbasalaev@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package alchemy.platform; import alchemy.fs.Filesystem; import alchemy.io.UTFReader; import alchemy.util.HashMap; import alchemy.util.Strings; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Common installation routines. * This class is used to acquire configuration * and make necessary preparations for boot, * install and update. * * <h4>Setup configuration</h4> * * Setup configuration is embedded in the program bundle * in file /setup.cfg and stores information about platform. * The following keys are defined in it: * <table border="1"> * <tr> * <th>Key</th> * <th>Value</th> * </tr> * <tr> * <td><code>version</code></td> * <td>Version of Alchemy OS bundle.</td> * </tr> * <tr> * <td><code>platform</code></td> * <td>Target platform for this bundle. Possible values * are <code>pc</code>, <code>j2me</code>, <code>android</code>.</td> * </tr> * <tr> * <td><code>install.archives</code></td> * <td>List of bundled archives to unpack on install/update.</td> * </tr> * <tr> * <td><code>install.fs.*.name</code></td> * <td>Name of the file system driver for the installer.</td> * </tr> * <tr> * <td><code>install.fs.*.init</code></td> * <td>Initial options for the file system driver.</td> * </tr> * <tr> * <td>install.fs.*.test</td> * <td>Name of the required class for the driver.</td> * </tr> * <tr> * <td>install.fs.*.nav</td> * <td>If set to true, the file system is navigable.</td> * </tr> * </table> * * <h4>Installation configuration</h4> * * Installation configuration is written when Alchemy OS * is being installed/updated. It contains the following * keys: * * <table border="1"> * <tr> * <th>Key</th> * <th>Value</th> * </tr> * <tr> * <td><code>version</code></td> * <td>Installed version of Alchemy OS.</td> * </tr> * <tr> * <td><code>fs.driver</code></td> * <td>The name of the driver used to mount root file system.</td> * </tr> * <tr> * <td><code>fs.options</code></td> * <td>The options to mount root file system.</td> * </tr> * </table> * * @author Sergey Basalaev */ public final class Installer { /** Configuration field that contains current version. */ public static final String VERSION = "version"; /** Configuration field that contains target platform. */ public static final String PLATFORM = "platform"; /** Configuration field that contains list of archives to unpack. */ public static final String ARCHIVES = "install.archives"; /** Configuration field that contains driver name for the root file system. */ public static final String FS_DRIVER = "fs.driver"; /** Configuration field that contains options for the root file system. */ public static final String FS_OPTIONS = "fs.options"; /** Configuration of installer. */ private final HashMap setupCfg; /** Platform dependent config methods. */ private final InstallCfg instCfg; public Installer() throws IOException { setupCfg = getSetupCfg(); instCfg = Platform.getPlatform().installCfg(); } /** Returns configuration embedded in the current bundle. */ public HashMap getSetupConfig() { return setupCfg; } /** Returns configuration stored on platform. */ public HashMap getInstalledConfig() { return instCfg.getConfig(); } /** Writes configuration modifications. */ public void saveInstalledConfig() throws IOException { instCfg.save(); } /** Purges configuration turning Alchemy OS in uninstalled state. */ public void removeInstalledConfig() throws IOException { instCfg.remove(); } /** Reads setup.cfg embedded in the jar. */ static HashMap getSetupCfg() throws IOException { InputStream cfgin = Installer.class.getResourceAsStream("/setup.cfg"); if (cfgin == null) throw new IOException("setup.cfg not found"); HashMap map = parseConfig(cfgin); cfgin.close(); return map; } /** Parses configuration and returns it as key-value pairs. */ public static HashMap parseConfig(InputStream input) throws IOException { HashMap map = new HashMap(); UTFReader r = new UTFReader(input); String line; while ((line = r.readLine()) != null) { int eq = line.indexOf('='); if (eq >= 0) { map.set(line.substring(0, eq).trim(), line.substring(eq+1).trim()); } } return map; } /** Compares version strings. */ public static int compareVersions(String v1, String v2) { String[] v1parts = Strings.split(v1, '.', false); String[] v2parts = Strings.split(v2, '.', false); int index = 0; while (true) { if (index < v1parts.length) { int i1 = Integer.parseInt(v1parts[index]); int i2 = (index < v2parts.length) ? Integer.parseInt(v2parts[index]) : 0; if (i1 != i2) return i1-i2; } else if (index < v2parts.length) { int i2 = Integer.parseInt(v2parts[index]); if (i2 != 0) return -i2; } else { return 0; // two versions are equal } index++; } } /** Returns true if Alchemy OS is installed. */ public boolean isInstalled() { return instCfg.exists(); } /** * Returns true if Alchemy OS is installed and its * version is older then the current. */ public boolean isUpdateNeeded() { return instCfg.exists() && compareVersions(instCfg.getConfig().get(VERSION).toString(), setupCfg.get(VERSION).toString()) < 0; } /** * Unpacks installer. * This method mounts root file system, unpacks base * files, unmounts it and writes install configuration. */ public void install(String fsdriver, String fsoptions) throws IOException { Filesystem.mount("", fsdriver, fsoptions); unpackBaseSystem(); Filesystem.unmount(""); HashMap cfg = instCfg.getConfig(); cfg.set(VERSION, setupCfg.get(VERSION)); cfg.set(FS_DRIVER, fsdriver); cfg.set(FS_OPTIONS, fsoptions); instCfg.save(); } /** * Unpacks installer and updates configuration. */ public void update() throws IOException { HashMap cfg = instCfg.getConfig(); Filesystem.mount("", cfg.get(FS_DRIVER).toString(), cfg.get(FS_OPTIONS).toString()); unpackBaseSystem(); Filesystem.unmount(""); cfg.set(VERSION, setupCfg.get(VERSION)); instCfg.save(); } /** * Installs base files in the file system. * File system must be mounted before this method. */ private void unpackBaseSystem() throws IOException { String[] archives = Strings.split(getSetupCfg().get(ARCHIVES).toString(), ' ', true); for (int i=0; i<archives.length; i++) { String arh = archives[i]; DataInputStream datastream = new DataInputStream(getClass().getResourceAsStream("/"+arh)); while (datastream.available() > 0) { String fname = datastream.readUTF(); String f = '/'+fname; datastream.skip(8); //timestamp int attrs = datastream.readUnsignedByte(); if ((attrs & 16) != 0) { //directory if (!Filesystem.exists(f)) Filesystem.mkdir(f); } else { if (!Filesystem.exists(f)) Filesystem.create(f); byte[] data = new byte[datastream.readInt()]; datastream.readFully(data); OutputStream out = Filesystem.write(f); out.write(data); out.flush(); out.close(); } Filesystem.setExec(f, (attrs & 1) != 0); } datastream.close(); } if (Filesystem.exists("/PACKAGE")) Filesystem.remove("/PACKAGE"); } }