com.yifanlu.PSXperiaTool.Extractor.CrashBandicootExtractor.java Source code

Java tutorial

Introduction

Here is the source code for com.yifanlu.PSXperiaTool.Extractor.CrashBandicootExtractor.java

Source

/*
 * PSXperia Converter Tool - Extractor
 * Copyright (C) 2011 Yifan Lu (http://yifan.lu/)
 *
 * 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 com.yifanlu.PSXperiaTool.Extractor;

import brut.androlib.AndrolibException;
import brut.androlib.res.AndrolibResources;
import brut.androlib.res.data.ResTable;
import brut.androlib.res.util.ExtFile;
import com.yifanlu.PSXperiaTool.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import ie.wombat.jbdiff.JBPatch;

import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class CrashBandicootExtractor extends ProgressMonitor {
    private static final int TOTAL_STEPS = 8;
    private File mApkFile;
    private File mZpakData;
    private File mOutputDir;
    private static final int BLOCK_SIZE = 1024;
    private static final Map<String, String> STRING_REPLACEMENT_MAP = new TreeMap<String, String>();

    public CrashBandicootExtractor(File apk, File zPakData, File outputDir) {
        this.mApkFile = apk;
        this.mOutputDir = outputDir;
        this.mZpakData = zPakData;
        setTotalSteps(TOTAL_STEPS);
    }

    public void extractApk() throws IOException, URISyntaxException {
        Logger.info("Starting extraction with PSXPeria Extractor version %s", PSXperiaTool.VERSION);
        verifyFiles();
        processConfig();
        decodeValues();
        FileFilter filterCompiledRes = new FileFilter() {
            public boolean accept(File file) {
                if (file.getParent() == null || file.getParentFile().getParent() == null)
                    return true;
                File parent = file.getParentFile();
                return (!parent.getName().equals("res")) && (!parent.getParentFile().getName().equals("res"));
            }
        };
        nextStep("Extracting APK");
        extractZip(mApkFile, mOutputDir, filterCompiledRes);
        extractZpaks();
        cleanUp();
        moveResourceFiles();
        patchStrings();
        patchEmulator();
        nextStep("Done.");
    }

    private void verifyFiles() throws IOException {
        nextStep("Verifying files");
        if (!mApkFile.exists())
            throw new FileNotFoundException("Cannot find APK file: " + mApkFile.getPath());
        if (!mZpakData.exists())
            throw new FileNotFoundException("Cannot find ZPAK file: " + mZpakData.getPath());
        if (!mOutputDir.exists())
            mOutputDir.mkdirs();
        if (mOutputDir.list().length > 0)
            Logger.warning(
                    "The output directory is not empty! Whatever is in this folder will be included in all generated APKs.");
        //FileUtils.cleanDirectory(mOutputDir);
    }

    private void processConfig() throws IOException, UnsupportedOperationException {
        long crc32 = ZpakCreate.getCRC32(mApkFile);
        String crcString = Long.toHexString(crc32).toUpperCase();
        InputStream inConfig = null;
        if ((inConfig = PSXperiaTool.class
                .getResourceAsStream("/resources/patches/" + crcString + "/config.xml")) == null) {
            throw new FileNotFoundException("Cannot find config for this APK (CRC32: " + crcString + ")");
        }
        Properties config = new Properties();
        config.loadFromXML(inConfig);
        inConfig.close();
        Logger.info("Identified " + config.getProperty("game_name", "Unknown Game") + " "
                + config.getProperty("game_region") + " Version " + config.getProperty("game_version", "Unknown")
                + ", CRC32: " + config.getProperty("game_crc32", "Unknown"));
        if (config.getProperty("valid", "yes").equals("no"))
            throw new UnsupportedOperationException("This APK is not supported.");
        Logger.verbose("Copying config files.");
        FileUtils.copyInputStreamToFile(
                PSXperiaTool.class.getResourceAsStream("/resources/patches/" + crcString + "/config.xml"),
                new File(mOutputDir, "/config/config.xml"));
        FileUtils.copyInputStreamToFile(
                PSXperiaTool.class.getResourceAsStream("/resources/patches/" + crcString + "/filelist.txt"),
                new File(mOutputDir, "/config/filelist.txt"));
        FileUtils.copyInputStreamToFile(
                PSXperiaTool.class
                        .getResourceAsStream("/resources/patches/" + crcString + "/stringReplacements.txt"),
                new File(mOutputDir, "/config/stringReplacements.txt"));
        String emulatorPatch = config.getProperty("emulator_patch", "");
        String gamePatch = config.getProperty("iso_patch", "");
        if (!gamePatch.equals("")) {
            FileUtils.copyInputStreamToFile(
                    PSXperiaTool.class.getResourceAsStream("/resources/patches/" + crcString + "/" + gamePatch),
                    new File(mOutputDir, "/config/game-patch.bin"));
        }
        if (!emulatorPatch.equals("")) {
            FileUtils.copyInputStreamToFile(
                    PSXperiaTool.class.getResourceAsStream("/resources/patches/" + crcString + "/" + emulatorPatch),
                    new File(mOutputDir, "/config/" + emulatorPatch));
        }
    }

    private void extractZip(File zipFile, File output, FileFilter filter) throws IOException {
        Logger.info("Extracting ZIP file: %s to: %s", zipFile.getPath(), output.getPath());
        if (!output.exists())
            output.mkdirs();
        ZipInputStream zip = new ZipInputStream(new FileInputStream(zipFile));
        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            File file = new File(output, entry.getName());
            if (file.isDirectory())
                continue;
            if (filter != null && !filter.accept(file))
                continue;
            Logger.verbose("Unzipping %s", entry.getName());
            FileUtils.touch(file);
            FileOutputStream out = new FileOutputStream(file.getPath());
            int n;
            byte[] buffer = new byte[BLOCK_SIZE];
            while ((n = zip.read(buffer)) != -1) {
                out.write(buffer, 0, n);
            }
            out.close();
            zip.closeEntry();
            Logger.verbose("Done extracting %s", entry.getName());
        }
        zip.close();
        Logger.debug("Done extracting ZIP.");
    }

    private void extractZpaks() throws IOException {
        nextStep("Extracting ZPAKS");
        WildcardFileFilter ff = new WildcardFileFilter("*.zpak");
        File[] candidates = (new File(mOutputDir, "/assets")).listFiles((FileFilter) ff);
        if (candidates == null || candidates.length < 1)
            throw new FileNotFoundException("Cannot find the default ZPAK under /assets");
        else if (candidates.length > 1)
            Logger.warning("Found more than one default ZPAK under /assets. Using the first one.");
        File defaultZpak = candidates[0];
        extractZip(defaultZpak, new File(mOutputDir, "/assets/ZPAK"), null);
        FileFilter filter = new FileFilter() {
            public boolean accept(File file) {
                if (file.getName().equals("image.ps"))
                    return false;
                if (file.getParentFile() == null)
                    return true;
                if (file.getParentFile().getName().equals("manual"))
                    return false;
                return true;
            }
        };
        extractZip(mZpakData, new File(mOutputDir, "/ZPAK"), filter);
        defaultZpak.delete();
    }

    private void decodeValues() throws IOException {
        nextStep("Decoding values");
        try {
            AndrolibResources res = new AndrolibResources();
            ExtFile extFile = new ExtFile(mApkFile);
            ResTable resTable = res.getResTable(extFile);
            res.decode(resTable, extFile, mOutputDir);
        } catch (AndrolibException ex) {
            ex.printStackTrace();
            throw new IOException(ex);
        }
    }

    private void cleanUp() throws IOException {
        nextStep("Removing unneeded files.");
        (new File(mOutputDir, "/AndroidManifest.xml")).delete();
        FileUtils.deleteDirectory(new File(mOutputDir, "/META-INF"));
        Logger.verbose("Done cleaning up.");
    }

    private void moveResourceFiles() throws IOException {
        nextStep("Adding new files.");
        InputStream defaultIcon = PSXperiaTool.class.getResourceAsStream("/resources/icon.png");
        writeStreamToFile(defaultIcon, new File(mOutputDir, "/assets/ZPAK/assets/default/bitmaps/icon.png"));
        defaultIcon = PSXperiaTool.class.getResourceAsStream("/resources/icon.png");
        writeStreamToFile(defaultIcon, new File(mOutputDir, "/res/drawable/icon.png"));
        defaultIcon.close();
        Logger.verbose("Done adding new files.");
    }

    private void writeStreamToFile(InputStream in, File outFile) throws IOException {
        Logger.verbose("Writing to: %s", outFile.getPath());
        FileOutputStream out = new FileOutputStream(outFile);
        byte[] buffer = new byte[BLOCK_SIZE];
        int n;
        while ((n = in.read(buffer)) != -1) {
            out.write(buffer, 0, n);
        }
        out.close();
    }

    private void fillReplacementMap() throws IOException {
        Logger.verbose("Filling string replacement map with resource data.");
        File file = new File(mOutputDir, "/config/stringReplacements.txt");
        InputStream in = new FileInputStream(file);
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String line1, line2;
        while ((line1 = reader.readLine()) != null && (line2 = reader.readLine()) != null) {
            if (line1.isEmpty())
                continue;
            Logger.verbose("Replacing %s with %s.", line1, line2);
            STRING_REPLACEMENT_MAP.put(line1, line2);
        }
        reader.close();
        in.close();
        file.delete();
    }

    private void patchStrings() throws IOException {
        nextStep("Patching XML strings with tags.");
        if (STRING_REPLACEMENT_MAP.isEmpty()) {
            fillReplacementMap();
        }
        StringReplacement strReplace = new StringReplacement(STRING_REPLACEMENT_MAP, mOutputDir);
        strReplace.execute(PSXperiaTool.FILES_TO_MODIFY);
        Logger.verbose("String replacement done.");
    }

    private void patchEmulator() throws IOException {
        Logger.info("Verifying the emulator binary.");
        Properties config = new Properties();
        config.loadFromXML(new FileInputStream(new File(mOutputDir, "/config/config.xml")));
        String emulatorName = config.getProperty("emulator_name", "libjava-activity.so");
        File origEmulator = new File(mOutputDir, "/lib/armeabi/" + emulatorName);
        String emulatorCRC32 = Long.toHexString(FileUtils.checksumCRC32(origEmulator));
        if (!emulatorCRC32.equalsIgnoreCase(config.getProperty("emulator_crc32")))
            throw new UnsupportedOperationException(
                    "The emulator checksum is invalid. Cannot patch. CRC32: " + emulatorCRC32);
        File newEmulator = new File(mOutputDir, "/lib/armeabi/libjava-activity-patched.so");
        File emulatorPatch = new File(mOutputDir, "/config/" + config.getProperty("emulator_patch", ""));
        if (emulatorPatch.equals("")) {
            Logger.info("No patch needed.");
            FileUtils.moveFile(origEmulator, newEmulator);
        } else {
            Logger.info("Patching emulator.");
            newEmulator.createNewFile();
            JBPatch.bspatch(origEmulator, newEmulator, emulatorPatch);
            emulatorPatch.delete();
        }
        FileUtils.copyInputStreamToFile(
                PSXperiaTool.class.getResourceAsStream("/resources/libjava-activity-wrapper.so"), origEmulator);
    }
}