Support EBP patches

This commit is contained in:
Boris Timofeev 2016-12-19 15:48:44 +03:00
parent bf438bde16
commit d39682952d
17 changed files with 307 additions and 19 deletions

View file

@ -38,6 +38,7 @@
<data android:pathPattern="/.*\\.ups" />
<data android:pathPattern="/.*\\.bps" />
<data android:pathPattern="/.*\\.ppf" />
<data android:pathPattern="/.*\\.ebp" />
<data android:pathPattern="/.*\\.dps" />
<data android:pathPattern="/.*\\.xdelta" />
<data android:pathPattern="/.*\\.xdelta3" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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());

View file

@ -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);

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> 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);
}
}
}

View file

@ -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];

View file

@ -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<String, String> 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();
}
}
}

View file

@ -1,5 +1,5 @@
<body>
<p>UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.</p>
<p>UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.</p>
<p>Additional features:</p>
<ul>
<li>Fix checksum in Sega Mega Drive ROMs</li>

View file

@ -1,5 +1,10 @@
<body>
<h4>0.11 (December, 2016)</h4>
<ul>
<li>Support EBP patches (for EarthBound game)</li>
</ul>
<h4>0.10.1 (December 6, 2016)</h4>
<ul>
<li>Added dark theme</li>

View file

@ -6,7 +6,7 @@
<p>UniPatcher is an Android tool for applying patches to ROM images of various video game consoles.</p>
<h4>What patch formats are supported?</h4>
<p>The app supports IPS, UPS, BPS, PPF, DPS and XDelta3 patches.</p>
<p>The app supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patches.</p>
<h4>Can I hack or crack Android game using this app?</h4>
<p>No. UniPatcher is not designed to hack the Android games.</p>

View file

@ -64,6 +64,7 @@
<string name="notify_error_not_ups_patch">Not a valid UPS patch</string>
<string name="notify_error_not_bps_patch">Not a valid BPS patch</string>
<string name="notify_error_not_ppf_patch">Not a valid PPF patch</string>
<string name="notify_error_not_ebp_patch">Not a valid EBP patch</string>
<string name="notify_error_not_dps_patch">Not a valid DPS patch</string>
<string name="notify_error_not_xdelta3_patch">Not a valid XDelta3 patch</string>
<string name="notify_error_xdelta1_unsupported">I am not able to work with XDelta1 patches</string>
@ -123,7 +124,7 @@
<string name="send_feedback_error_no_email_apps">There are no email clients installed</string>
<string name="share_dialog_title">Share UniPatcher</string>
<string name="share_text">Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.\n\n</string>
<string name="share_text">Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.\n\n</string>
<!-- Navigation list -->
<string name="nav_apply_patch">Apply patch</string>

View file

@ -12,6 +12,7 @@ Utility to apply patches to game rom.
&bull; BPS
&bull; PPF
&bull; DPS
&bull; EBP
&bull; XDelta 3