Java tutorial
/* * PSXperia Converter Tool - Main backend * 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; import com.android.sdklib.internal.build.SignedJarBuilder; import org.apache.commons.io.FileUtils; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.GeneralSecurityException; import java.util.*; public class PSXperiaTool extends ProgressMonitor { public static final String VERSION = "2.0 Beta"; public static final String[] FILES_TO_MODIFY = { //"/AndroidManifest.xml", "/assets/AndroidManifest.xml", "/assets/ZPAK/metadata.xml", "/res/values/strings.xml", "/ZPAK/metadata.xml" }; private File mInputFile; private File mDataDir; private File mTempDir = null; private File mOutputDir; private Properties mProperties; private static final int TOTAL_STEPS = 9; public PSXperiaTool(Properties properties, File inputFile, File dataDir, File outputDir) { mInputFile = inputFile; mDataDir = dataDir; mProperties = properties; mOutputDir = outputDir; Logger.info("PSXperiaTool initialized, outputting to: %s", mOutputDir.getPath()); setTotalSteps(TOTAL_STEPS); } public void startBuild() throws IOException, InterruptedException, GeneralSecurityException, SignedJarBuilder.IZipEntryFilter.ZipAbortException { Logger.info("Starting build with PSXPeria Converter version %s", VERSION); checkData(mDataDir); mTempDir = createTempDir(mDataDir); copyIconImage((File) mProperties.get("IconFile")); //BuildResources br = new BuildResources(mProperties, mTempDir); replaceStrings(); patchGame(); generateImage(); generateDefaultZpak(); //buildResources(br); generateOutput(); nextStep("Deleting temporary directory"); FileUtils.deleteDirectory(mTempDir); nextStep("Done."); } private File createTempDir(File dataDir) throws IOException { nextStep("Creating temporary directory."); File tempDir = new File(new File("."), "/.psxperia." + (int) (Math.random() * 1000)); if (tempDir.exists()) FileUtils.deleteDirectory(tempDir); if (!tempDir.mkdirs()) throw new IOException("Cannot create temporary directory!"); FileUtils.copyDirectory(dataDir, tempDir); Logger.debug("Created temporary directory at, %s", tempDir.getPath()); return tempDir; } private void checkData(File dataDir) throws IllegalArgumentException, IOException { nextStep("Checking to make sure all files are there."); if (!mDataDir.exists()) throw new FileNotFoundException("Cannot find data directory!"); File filelist = new File(mDataDir, "/config/filelist.txt"); if (!filelist.exists()) throw new FileNotFoundException("Cannot find list to validate files!"); InputStream fstream = new FileInputStream(filelist); BufferedReader reader = new BufferedReader(new InputStreamReader(fstream)); String line; while ((line = reader.readLine()) != null) { if (line.isEmpty()) continue; File check = new File(mDataDir, line); if (!check.exists()) throw new IllegalArgumentException("Cannot find required data file: " + line); } Properties config = new Properties(); config.loadFromXML(new FileInputStream(new File(mDataDir, "/config/config.xml"))); Logger.info("Using data from " + config.getProperty("game_name", "Unknown Game") + " " + config.getProperty("game_region") + " Version " + config.getProperty("game_version", "Unknown") + ", CRC32: " + config.getProperty("game_crc32", "Unknown")); Logger.debug("Done checking data."); } private void copyIconImage(File image) throws IllegalArgumentException, IOException { nextStep("Copying icon if needed."); if (image == null || !(image instanceof File)) { Logger.verbose("Icon copying not needed."); return; } if (!image.exists()) throw new IllegalArgumentException("Icon file not found."); FileUtils.copyFile(image, new File(mTempDir, "/res/drawable/icon.png")); FileUtils.copyFile(image, new File(mTempDir, "/assets/ZPAK/assets/default/bitmaps/icon.png")); Logger.debug("Done copying icon from %s", image.getPath()); } public void replaceStrings() throws IOException { nextStep("Replacing strings."); Map<String, String> replacement = new TreeMap<String, String>(); Iterator<Object> it = mProperties.keySet().iterator(); while (it.hasNext()) { String key = (String) it.next(); Object value = mProperties.getProperty(key); if (!(value instanceof String)) continue; if (!(key.startsWith("KEY_"))) continue; String find = ((String) key).substring("KEY_".length()); Logger.verbose("Found replacement key: %s, replacing with: %s", find, value); replacement.put("\\{" + find + "\\}", (String) value); replacement.put("\\{FILTERED_" + find + "\\}", StringReplacement.filter((String) value)); } StringReplacement strReplace = new StringReplacement(replacement, mTempDir); strReplace.execute(FILES_TO_MODIFY); Logger.debug("Done replacing strings."); } private void patchGame() throws IOException { /* * Custom patch format (config/game-patch.bin) is as follows: * 0x8 byte little endian: Address in game image to start patching * 0x8 byte little endian: Length of patch * If there are more patches, repeat after reading the length of patch * Note that all games will be patched the same way, so if a game is broken before patching, it will still be broken! */ nextStep("Patching game."); File gamePatch = new File(mTempDir, "/config/game-patch.bin"); if (!gamePatch.exists()) return; Logger.info("Making a copy of game."); File tempGame = new File(mTempDir, "game.iso"); FileUtils.copyFile(mInputFile, tempGame); RandomAccessFile game = new RandomAccessFile(tempGame, "rw"); InputStream patch = new FileInputStream(gamePatch); while (true) { byte[] rawPatchAddr = new byte[8]; byte[] rawPatchLen = new byte[8]; if (patch.read(rawPatchAddr) + patch.read(rawPatchLen) < rawPatchAddr.length + rawPatchLen.length) break; ByteBuffer bb = ByteBuffer.wrap(rawPatchAddr); bb.order(ByteOrder.LITTLE_ENDIAN); long patchAddr = bb.getLong(); bb = ByteBuffer.wrap(rawPatchLen); bb.order(ByteOrder.LITTLE_ENDIAN); long patchLen = bb.getLong(); game.seek(patchAddr); while (patchLen-- > 0) { game.write(patch.read()); } } mInputFile = tempGame; game.close(); patch.close(); Logger.debug("Done patching game."); } private void generateImage() throws IOException { nextStep("Generating PSImage."); FileInputStream in = new FileInputStream(mInputFile); FileOutputStream out = new FileOutputStream(new File(mTempDir, "/ZPAK/data/image.ps")); FileOutputStream tocOut = new FileOutputStream(new File(mTempDir, "/image_ps_toc.bin")); PSImageCreate ps = new PSImageCreate(in); PSImage.ProgressCallback progress = new PSImage.ProgressCallback() { int mBytesRead = 0, mBytesWritten = 0; public void bytesReadChanged(int delta) { mBytesRead += delta; jump(mBytesRead); Logger.verbose("Image bytes read: %d", mBytesRead); } public void bytesWrittenChanged(int delta) { mBytesWritten += delta; Logger.verbose("Compressed PSImage bytes written: %d", mBytesWritten); } }; // progress management int oldSteps = getSteps(); setTotalSteps((int) in.getChannel().size()); jump(0); ps.setCallback(progress); ps.compress(out); ps.writeTocTable(tocOut); out.close(); tocOut.close(); in.close(); setTotalSteps(TOTAL_STEPS); jump(oldSteps); Logger.debug("Done generating PSImage"); Logger.debug("Deleting temporary patched game."); FileUtils.deleteQuietly(new File(mTempDir, "game.iso")); Logger.info("Generating ZPAK."); File zpakDirectory = new File(mTempDir, "/ZPAK"); File zpakFile = new File(mTempDir, "/" + mProperties.getProperty("KEY_TITLE_ID") + ".zpak"); FileOutputStream zpakOut = new FileOutputStream(zpakFile); ZpakCreate zcreate = new ZpakCreate(zpakOut, zpakDirectory); zcreate.create(true); FileUtils.deleteDirectory(zpakDirectory); Logger.debug("Done generating ZPAK at %s", zpakFile.getPath()); } private void generateDefaultZpak() throws IOException { nextStep("Generating default ZPAK."); File defaultZpakDirectory = new File(mTempDir, "/assets/ZPAK"); File zpakFile = new File(mTempDir, "/assets/" + mProperties.getProperty("KEY_TITLE_ID") + ".zpak"); FileOutputStream zpakOut = new FileOutputStream(zpakFile); ZpakCreate zcreate = new ZpakCreate(zpakOut, defaultZpakDirectory); zcreate.create(false); zpakOut.close(); FileUtils.deleteDirectory(defaultZpakDirectory); Logger.debug("Done generating default ZPAK at %s", zpakFile.getPath()); } private void generateOutput() throws IOException, InterruptedException, GeneralSecurityException, SignedJarBuilder.IZipEntryFilter.ZipAbortException { nextStep("Done processing, generating output."); String titleId = mProperties.getProperty("KEY_TITLE_ID"); if (!mOutputDir.exists()) mOutputDir.mkdir(); File outDataDir = new File(mOutputDir, "/data/com.sony.playstation." + titleId + "/files/content"); if (!outDataDir.exists()) outDataDir.mkdirs(); Logger.debug("Moving files around."); FileUtils.cleanDirectory(outDataDir); FileUtils.moveFileToDirectory(new File(mTempDir, "/" + titleId + ".zpak"), outDataDir, false); FileUtils.moveFileToDirectory(new File(mTempDir, "/image_ps_toc.bin"), outDataDir, false); Logger.verbose("Deleting config from temp directory."); FileUtils.deleteDirectory(new File(mTempDir, "/config")); File outApk = new File(mOutputDir, "/com.sony.playstation." + titleId + ".apk"); ApkBuilder build = new ApkBuilder(mTempDir, outApk); build.buildApk(); Logger.info("Done."); Logger.info("APK file: %s", outApk.getPath()); Logger.info("Data dir: %s", outDataDir.getPath()); } }