diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 423a369..15e85e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ + diff --git a/app/src/main/assets/patch/ebp/wrong1.ips b/app/src/main/assets/patch/ebp/wrong1.ips new file mode 100644 index 0000000..8e59ea6 Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong1.ips differ diff --git a/app/src/main/assets/patch/ebp/wrong2.ips b/app/src/main/assets/patch/ebp/wrong2.ips new file mode 100644 index 0000000..17264c0 Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong2.ips differ diff --git a/app/src/main/assets/patch/ebp/wrong3.ips b/app/src/main/assets/patch/ebp/wrong3.ips new file mode 100644 index 0000000..6e14945 Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong3.ips differ diff --git a/app/src/main/assets/patch/ebp/wrong4.ips b/app/src/main/assets/patch/ebp/wrong4.ips new file mode 100644 index 0000000..a9a5350 Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong4.ips differ diff --git a/app/src/main/assets/patch/ebp/wrong5.ips b/app/src/main/assets/patch/ebp/wrong5.ips new file mode 100644 index 0000000..a70b0b2 Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong5.ips differ diff --git a/app/src/main/assets/patch/ebp/wrong6.ips b/app/src/main/assets/patch/ebp/wrong6.ips new file mode 100644 index 0000000..0b9c91a Binary files /dev/null and b/app/src/main/assets/patch/ebp/wrong6.ips differ diff --git a/app/src/main/java/org/emunix/unipatcher/Utils.java b/app/src/main/java/org/emunix/unipatcher/Utils.java index 3c347ca..c8f9f0d 100644 --- a/app/src/main/java/org/emunix/unipatcher/Utils.java +++ b/app/src/main/java/org/emunix/unipatcher/Utils.java @@ -139,6 +139,14 @@ public class Utils { } } + public static String bytesToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < bytes.length ;i++) { + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + return sb.toString(); + } + public static boolean isPatch(File file) { String[] patches = {"ips", "ups", "bps", "ppf", "dps", "xdelta", "xdelta3", "vcdiff"}; String ext = FilenameUtils.getExtension(file.getName()).toLowerCase(Locale.getDefault()); diff --git a/app/src/main/java/org/emunix/unipatcher/WorkerService.java b/app/src/main/java/org/emunix/unipatcher/WorkerService.java index e19fa8b..86339f9 100644 --- a/app/src/main/java/org/emunix/unipatcher/WorkerService.java +++ b/app/src/main/java/org/emunix/unipatcher/WorkerService.java @@ -31,6 +31,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.emunix.unipatcher.patch.BPS; import org.emunix.unipatcher.patch.DPS; +import org.emunix.unipatcher.patch.EBP; import org.emunix.unipatcher.patch.IPS; import org.emunix.unipatcher.patch.PPF; import org.emunix.unipatcher.patch.Patch; @@ -118,6 +119,8 @@ public class WorkerService extends IntentService { patcher = new BPS(this, patchFile, romFile, outputFile); else if ("ppf".equals(ext)) patcher = new PPF(this, patchFile, romFile, outputFile); + else if ("ebp".equals(ext)) + patcher = new EBP(this, patchFile, romFile, outputFile); else if ("dps".equals(ext)) patcher = new DPS(this, patchFile, romFile, outputFile); else if ("xdelta".equals(ext) || "xdelta3".equals(ext) || "vcdiff".equals(ext)) @@ -218,7 +221,7 @@ public class WorkerService extends IntentService { startForeground(notify.getID(), notify.getNotifyBuilder().build()); try { - worker.deleteSnesSmcHeader(this, romFile); + worker.deleteSnesSmcHeader(this, romFile, true); } catch (RomException | IOException e) { if (Utils.getFreeSpace(romFile.getParentFile()) == 0) { errorMsg = getString(R.string.notify_error_not_enough_space); diff --git a/app/src/main/java/org/emunix/unipatcher/patch/EBP.java b/app/src/main/java/org/emunix/unipatcher/patch/EBP.java new file mode 100644 index 0000000..7193d96 --- /dev/null +++ b/app/src/main/java/org/emunix/unipatcher/patch/EBP.java @@ -0,0 +1,275 @@ +/* +This file based on source code of EBPatcher by Marc Gagné (https://github.com/Lyrositor/EBPatcher) + +Copyright (C) 2016 Boris Timofeev + +This file is part of UniPatcher. + +UniPatcher 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. + +UniPatcher 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 UniPatcher. If not, see . +*/ + +package org.emunix.unipatcher.patch; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.emunix.unipatcher.R; +import org.emunix.unipatcher.Utils; +import org.emunix.unipatcher.tools.RomException; +import org.emunix.unipatcher.tools.SnesSmcHeader; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; + +public class EBP extends Patch { + + private static final byte[] MAGIC_NUMBER = {0x50, 0x41, 0x54, 0x43, 0x48}; // "PATCH" + private static final byte[] EARTH_BOUND = {0x45, 0x41, 0x52, 0x54, 0x48, 0x20, 0x42, 0x4f, 0x55, 0x4e, 0x44}; + private static final String EB_CLEAN_MD5 = "a864b2e5c141d2dec1c4cbed75a42a85"; + private static final int EB_CLEAN_ROM_SIZE = 0x300000; + private static final HashMap EB_WRONG_MD5; + + static { + EB_WRONG_MD5 = new HashMap<>(); + EB_WRONG_MD5.put("8c28ce81c7d359cf9ccaa00d41f8ad33", "patch/ebp/wrong1.ips"); + EB_WRONG_MD5.put("b2dcafd3252cc4697bf4b89ea3358cd5", "patch/ebp/wrong2.ips"); + EB_WRONG_MD5.put("0b8c04fc0182e380ff0e3fe8fdd3b183", "patch/ebp/wrong3.ips"); + EB_WRONG_MD5.put("2225f8a979296b7dcccdda17b6a4f575", "patch/ebp/wrong4.ips"); + EB_WRONG_MD5.put("eb83b9b6ea5692cefe06e54ea3ec9394", "patch/ebp/wrong5.ips"); + EB_WRONG_MD5.put("cc9fa297e7bf9af21f7f179e657f1aa1", "patch/ebp/wrong6.ips"); + } + + public EBP(Context context, File patch, File rom, File output) { + super(context, patch, rom, output); + } + + @Override + public void apply() throws PatchException, IOException { + File cleanRom = File.createTempFile("rom", null, context.getCacheDir()); + File ipsPatch = File.createTempFile("patch", null, context.getCacheDir()); + try { + Utils.copyFile(context, romFile, cleanRom); + prepareCleanRom(cleanRom); + + EBPtoIPS(patchFile, ipsPatch); + + IPS ips = new IPS(context, ipsPatch, cleanRom, outputFile); + ips.apply(); + } finally { + FileUtils.deleteQuietly(ipsPatch); + FileUtils.deleteQuietly(cleanRom); + } + } + + private void prepareCleanRom(File file) throws IOException, PatchException { + // delete smc header + SnesSmcHeader smc = new SnesSmcHeader(); + try { + smc.deleteSnesSmcHeader(context, file, false); + } catch (RomException e) { + // no header + } + + // check rom size and remove unused expanded space + if (file.length() < EB_CLEAN_ROM_SIZE) + throw new PatchException(context.getString(R.string.notify_error_rom_not_compatible_with_patch)); + if (file.length() > EB_CLEAN_ROM_SIZE && checkExpanded(file)) + removeExpanded(file); + + // try to fix the ROM if it's incorrect + if (!checkMD5(file)) + repairRom(file); + + // if we couldn't fix the ROM, try to remove a 0xff byte at the end. + if (!checkMD5(file)) { + int length = (int)file.length(); + byte[] buffer = new byte[length]; + FileInputStream in = new FileInputStream(file); + int count = in.read(buffer); + in.close(); + if (count != file.length()) + throw new IOException("Unable read file"); + if (buffer[length - 1] == 0xff) + buffer[length - 1] = 0; + + if (checkMD5(buffer)) { + RandomAccessFile f = new RandomAccessFile(file, "rw"); + f.seek(length - 1); + f.write(0); + f.close(); + } + } + + if (!checkMD5(file) || !checkEarthBound(file)) { + throw new PatchException(context.getString(R.string.notify_error_rom_not_compatible_with_patch)); + } + } + + private boolean checkExpanded(File file) throws IOException { + byte[] byteArray = new byte[EB_CLEAN_ROM_SIZE]; + FileInputStream f = new FileInputStream(file); + int count = f.read(byteArray); + IOUtils.closeQuietly(f); + if (count < EB_CLEAN_ROM_SIZE) + throw new IOException("Unable to read 0x300000 bytes from ROM"); + // ExHiROM expanded ROMs have two bytes different from LoROM. + byteArray[0xffd5] = 0x31; + byteArray[0xffd7] = 0x0c; + + // If the normal area is unmodified, then the expanded area is unused and can be deleted. + return checkMD5(byteArray); + } + + private void removeExpanded(File file) throws IOException { + if (file.length() > 0x400000) { + RandomAccessFile f = new RandomAccessFile(file, "rw"); + f.seek(0xffd5); + f.write(0x31); + f.seek(0xffd7); + f.write(0x0c); + f.close(); + } + FileChannel fc = new FileOutputStream(file, true).getChannel(); + fc.truncate(EB_CLEAN_ROM_SIZE); + fc.close(); + } + + private void repairRom(File file) throws IOException, PatchException { + String md5 = calculateMD5(file); + if (EB_WRONG_MD5.containsKey(md5)) { + + // copy patch from assets + InputStream in = context.getAssets().open(EB_WRONG_MD5.get(md5)); + File patch = File.createTempFile("patch", null, context.getCacheDir()); + FileUtils.copyToFile(in, patch); + IOUtils.closeQuietly(in); + + // fix rom + File tmpFile = File.createTempFile("rom", null, context.getCacheDir()); + FileUtils.copyFile(file, tmpFile); + IPS ips = new IPS(context, patch, tmpFile, file); + ips.apply(); + + FileUtils.deleteQuietly(tmpFile); + FileUtils.deleteQuietly(patch); + } + } + + private boolean checkMD5(byte[] array) throws IOException { + try { + MessageDigest md5Digest = MessageDigest.getInstance("MD5"); + md5Digest.update(array); + String md5 = Utils.bytesToHexString(md5Digest.digest()); + return md5.equals(EB_CLEAN_MD5); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e.getMessage()); + } + } + + private boolean checkMD5(File file) throws IOException { + String md5 = calculateMD5(file); + return md5.equals(EB_CLEAN_MD5); + } + + @NonNull + private String calculateMD5(File file) throws IOException { + FileInputStream f = new FileInputStream(file); + try { + MessageDigest md5Digest = MessageDigest.getInstance("MD5"); + byte[] byteArray = new byte[32768]; + int count; + while ((count = f.read(byteArray)) != -1) + md5Digest.update(byteArray, 0, count); + return Utils.bytesToHexString(md5Digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e.getMessage()); + } finally { + IOUtils.closeQuietly(f); + } + } + + private boolean checkEarthBound(File file) throws IOException { + byte[] buffer = new byte[11]; + RandomAccessFile f = new RandomAccessFile(file, "r"); + f.seek(0xffc0); + f.read(buffer); + f.close(); + return Arrays.equals(EARTH_BOUND, buffer); + } + + private void EBPtoIPS(File ebpFile, File ipsFile) throws IOException, PatchException { + BufferedInputStream ebp = null; + BufferedOutputStream ips = null; + try { + ebp = new BufferedInputStream(new FileInputStream(ebpFile)); + ips = new BufferedOutputStream(new FileOutputStream(ipsFile)); + + int size; + byte[] buffer = new byte[65536]; + + if (ebpFile.length() < 14) { + throw new PatchException(context.getString(R.string.notify_error_patch_corrupted)); + } + + // check magic string + byte[] magic = new byte[5]; + size = ebp.read(magic); + if (size != 5 || !Arrays.equals(magic, MAGIC_NUMBER)) + throw new PatchException(context.getString(R.string.notify_error_not_ebp_patch)); + + ips.write(magic); + + while (true) { + size = ebp.read(buffer, 0, 3); + if (size < 3) + throw new PatchException(context.getString(R.string.notify_error_patch_corrupted)); + ips.write(buffer, 0, 3); + if (buffer[0] == 0x45 && buffer[1] == 0x4f && buffer[2] == 0x46) // EOF + break; + size = ebp.read(buffer, 0, 2); + if (size < 2) + throw new PatchException(context.getString(R.string.notify_error_patch_corrupted)); + ips.write(buffer, 0, 2); + size = (((int)buffer[0] & 0xff) << 8) + ((int)buffer[1] & 0xff); + if (size != 0) { + int c = ebp.read(buffer, 0, size); + if (c < size) + throw new PatchException(context.getString(R.string.notify_error_patch_corrupted)); + ips.write(buffer, 0, size); + } else { + size = ebp.read(buffer, 0, 3); + if (size < 3) + throw new PatchException(context.getString(R.string.notify_error_patch_corrupted)); + ips.write(buffer, 0, 3); + } + } + } finally { + IOUtils.closeQuietly(ips); + IOUtils.closeQuietly(ebp); + } + } +} diff --git a/app/src/main/java/org/emunix/unipatcher/tools/SnesSmcHeader.java b/app/src/main/java/org/emunix/unipatcher/tools/SnesSmcHeader.java index 5a81e25..7d35899 100644 --- a/app/src/main/java/org/emunix/unipatcher/tools/SnesSmcHeader.java +++ b/app/src/main/java/org/emunix/unipatcher/tools/SnesSmcHeader.java @@ -39,9 +39,9 @@ public class SnesSmcHeader { return (romSize & 0x7fff) == 512; } - public void deleteSnesSmcHeader(Context context, File romfile) throws IOException, RomException { + public void deleteSnesSmcHeader(Context context, File romfile, boolean saveHeader) throws IOException, RomException { if (!isHasSmcHeader(romfile)) { - throw new RomException(); + throw new RomException("ROM don't have SMC header"); } FileInputStream inputRom = null; @@ -51,17 +51,19 @@ public class SnesSmcHeader { try { tmpfile = File.createTempFile(romfile.getName(), null, romfile.getParentFile()); - File headerfile = new File(romfile.getPath()+".smc_header"); inputRom = new FileInputStream(romfile); outputRom = new FileOutputStream(tmpfile); - outputHeader = new FileOutputStream(headerfile); // write smc header in a file byte[] header = new byte[HEADER_SIZE]; int length; length = inputRom.read(header); - outputHeader.write(header, 0, length); + if (saveHeader) { + File headerfile = new File(romfile.getPath()+".smc_header"); + outputHeader = new FileOutputStream(headerfile); + outputHeader.write(header, 0, length); + } // write headerless rom in tmp file byte[] buffer = new byte[32768]; diff --git a/app/src/main/java/org/emunix/unipatcher/ui/activity/FilePickerActivity.java b/app/src/main/java/org/emunix/unipatcher/ui/activity/FilePickerActivity.java index f8fe238..85835c1 100644 --- a/app/src/main/java/org/emunix/unipatcher/ui/activity/FilePickerActivity.java +++ b/app/src/main/java/org/emunix/unipatcher/ui/activity/FilePickerActivity.java @@ -420,8 +420,8 @@ public class FilePickerActivity extends AppCompatActivity implements FilePickerA fis.close(); String crc32 = Long.toHexString(crc32Digest.getValue()); - String md5 = bytesToHexString(md5Digest.digest()); - String sha1 = bytesToHexString(sha1Digest.digest()); + String md5 = Utils.bytesToHexString(md5Digest.digest()); + String sha1 = Utils.bytesToHexString(sha1Digest.digest()); HashMap checksum = new HashMap<>(); checksum.put(CRC32, crc32); @@ -429,13 +429,5 @@ public class FilePickerActivity extends AppCompatActivity implements FilePickerA checksum.put(SHA1, sha1); return checksum; } - - private String bytesToHexString(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for(int i = 0; i < bytes.length ;i++) { - sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); - } - return sb.toString(); - } } } diff --git a/app/src/main/res/raw/about.html b/app/src/main/res/raw/about.html index 484e331..8b916ff 100644 --- a/app/src/main/res/raw/about.html +++ b/app/src/main/res/raw/about.html @@ -1,5 +1,5 @@ -

UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.

+

UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.

Additional features:

  • Fix checksum in Sega Mega Drive ROMs
  • diff --git a/app/src/main/res/raw/changelog.html b/app/src/main/res/raw/changelog.html index db0d72e..d382b6c 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -1,5 +1,10 @@ +

    0.11 (December, 2016)

    +
      +
    • Support EBP patches (for EarthBound game)
    • +
    +

    0.10.1 (December 6, 2016)

    • Added dark theme
    • diff --git a/app/src/main/res/raw/faq.html b/app/src/main/res/raw/faq.html index 76e898c..c01dc0e 100644 --- a/app/src/main/res/raw/faq.html +++ b/app/src/main/res/raw/faq.html @@ -6,7 +6,7 @@

      UniPatcher is an Android tool for applying patches to ROM images of various video game consoles.

      What patch formats are supported?

      -

      The app supports IPS, UPS, BPS, PPF, DPS and XDelta3 patches.

      +

      The app supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patches.

      Can I hack or crack Android game using this app?

      No. UniPatcher is not designed to hack the Android games.

      diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2542535..2b5f8b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ Not a valid UPS patch Not a valid BPS patch Not a valid PPF patch + Not a valid EBP patch Not a valid DPS patch Not a valid XDelta3 patch I am not able to work with XDelta1 patches @@ -123,7 +124,7 @@ There are no email clients installed Share UniPatcher - Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.\n\n + Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.\n\n Apply patch diff --git a/google-play/en/google-play.txt b/google-play/en/google-play.txt index ee6e6dc..32ce938 100644 --- a/google-play/en/google-play.txt +++ b/google-play/en/google-play.txt @@ -12,6 +12,7 @@ Utility to apply patches to game rom. • BPS • PPF • DPS +• EBP • XDelta 3