/* This file is part of jpcsp. Jpcsp 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. Jpcsp 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 Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.HLE.kernel.types; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.LinkedList; import java.util.List; import jpcsp.Memory; import jpcsp.memory.IMemoryReader; import jpcsp.memory.MemoryReader; import jpcsp.memory.IMemoryWriter; import jpcsp.memory.MemoryWriter; import jpcsp.HLE.Modules; import jpcsp.HLE.VFS.IVirtualFileSystem; import jpcsp.crypto.CryptoEngine; import jpcsp.filesystems.SeekableDataInput; import jpcsp.filesystems.SeekableRandomFile; import jpcsp.format.PNG; import jpcsp.format.PSF; import jpcsp.hardware.MemoryStick; import jpcsp.settings.Settings; import jpcsp.util.Utilities; public class SceUtilitySavedataParam extends pspUtilityBaseDialog { public final static String savedataPath = "ms0:/PSP/SAVEDATA/"; public final static String savedataFilePath = "ms0/PSP/SAVEDATA/"; public final static String icon0FileName = "ICON0.PNG"; public final static String icon1PNGFileName = "ICON1.PNG"; public final static String icon1PMFFileName = "ICON1.PMF"; public final static String pic1FileName = "PIC1.PNG"; public final static String snd0FileName = "SND0.AT3"; public final static String paramSfoFileName = "PARAM.SFO"; private final static String[] systemFileNames = { paramSfoFileName, icon0FileName, icon1PNGFileName, icon1PMFFileName, pic1FileName, snd0FileName }; public final static String anyFileName = "<>"; public int mode; public final static int MODE_AUTOLOAD = 0; public final static int MODE_AUTOSAVE = 1; public final static int MODE_LOAD = 2; public final static int MODE_SAVE = 3; public final static int MODE_LISTLOAD = 4; public final static int MODE_LISTSAVE = 5; public final static int MODE_LISTDELETE = 6; public final static int MODE_DELETE = 7; public final static int MODE_SIZES = 8; public final static int MODE_AUTODELETE = 9; public final static int MODE_SINGLEDELETE = 10; public final static int MODE_LIST = 11; public final static int MODE_FILES = 12; public final static int MODE_MAKEDATASECURE = 13; public final static int MODE_MAKEDATA = 14; public final static int MODE_READSECURE = 15; public final static int MODE_READ = 16; public final static int MODE_WRITESECURE = 17; public final static int MODE_WRITE = 18; public final static int MODE_ERASESECURE = 19; public final static int MODE_ERASE = 20; public final static int MODE_DELETEDATA = 21; public final static int MODE_GETSIZE = 22; public final static String[] modeNames = new String[]{ "AUTOLOAD", "AUTOSAVE", "LOAD", "SAVE", "LISTLOAD", "LISTSAVE", "LISTDELETE", "DELETE", "SIZES", "AUTODELETE", "SINGLEDELETE", "LIST", "FILES", "MAKEDATASECURE", "MAKEDATA", "READSECURE", "READ", "WRITESECURE", "WRITE", "ERASESECURE", "ERASE", "DELETEDATA", "GETSIZE" }; public int bind; // Used by certain applications to detect if this save data was created on a different PSP. public final static int BIND_NOT_USED = 0; public final static int BIND_IS_OK = 1; public final static int BIND_IS_REJECTED = 2; public final static int BIND_IS_NOT_SUPPORTED = 3; public boolean overwrite; public String gameName; // name used from the game for saves, equal for all saves public String saveName; // name of the particular save, normally a number public String fileName; // name of the data file of the game for example DATA.BIN public String[] saveNameList; // used by multiple modes public int saveNameListAddr; public int dataBuf; public int dataBufSize; public int dataSize; public PspUtilitySavedataSFOParam sfoParam; public PspUtilitySavedataFileData icon0FileData; public PspUtilitySavedataFileData icon1FileData; public PspUtilitySavedataFileData pic1FileData; public PspUtilitySavedataFileData snd0FileData; int newDataAddr; public PspUtilitySavedataListSaveNewData newData; public int focus; public final static int FOCUS_UNKNOWN = 0; public final static int FOCUS_FIRSTLIST = 1; // First in list public final static int FOCUS_LASTLIST = 2; // Last in list public final static int FOCUS_LATEST = 3; // Most recent one public final static int FOCUS_OLDEST = 4; // Oldest one public final static int FOCUS_FIRSTEMPTY = 7; // First empty slot public final static int FOCUS_LASTEMPTY = 8; // Last empty slot public int abortStatus; // Used by sceUtilityXXXAbort functions. public int msFreeAddr; // Address of a buffer to hold MemoryStick free size data (used in MODE_SIZES only). public int msDataAddr; // Address of a buffer to hold MemoryStick size data (used in MODE_SIZES only). public int utilityDataAddr; // Address of a buffer to hold utility size data (used in MODE_SIZES only). public byte[] key = new byte[0x10]; // Encrypt/decrypt key for saves with firmware >= 2.00. public int secureVersion; // 0 - Pre 2.00 (no encrypted files) / 1 - Post 2.00 (encrypted files are now used). public int multiStatus; // After 2.00, several modes can be triggered at the same time using this for sync. public final static int MULTI_STATUS_SINGLE = 0; // Save data is all generated in one call. public final static int MULTI_STATUS_INIT = 1; // Save data is generated in multiple calls and this is the first one. public final static int MULTI_STATUS_RELAY = 2; // Save data is generated in multiple calls and this is an intermediate call. public final static int MULTI_STATUS_FINISH = 3; // Save data is generated in multiple calls and this is the last one. public int idListAddr; // Address of a buffer to hold the file IDs generated by MODE_LIST. public int fileListAddr; // Address of a buffer to hold the file names generated by MODE_FILES. public int sizeAddr; // Address of a buffer to hold the sizes generated by MODE_GETSIZE. public static class PspUtilitySavedataSFOParam extends pspAbstractMemoryMappedStructure { public String title; public String savedataTitle; public String detail; public int parentalLevel; @Override protected void read() { title = readStringNZ(0x80); savedataTitle = readStringNZ(0x80); detail = readStringNZ(0x400); parentalLevel = read32(); } @Override protected void write() { writeStringNZ(0x80, title); writeStringNZ(0x80, savedataTitle); writeStringNZ(0x400, detail); write32(parentalLevel); } @Override public int sizeof() { return 0x80 + 0x80 + 0x400 + 4; } }; public static class PspUtilitySavedataFileData extends pspAbstractMemoryMappedStructure { public int buf; public int bufSize; public int size; @Override protected void read() { buf = read32(); bufSize = read32(); size = read32(); readUnknown(4); } @Override protected void write() { write32(buf); write32(bufSize); write32(size); writeUnknown(4); } @Override public int sizeof() { return 4 * 4; } } public static class PspUtilitySavedataListSaveNewData extends pspAbstractMemoryMappedStructure { public PspUtilitySavedataFileData icon0; public int titleAddr; public String title; @Override protected void read() { icon0 = new PspUtilitySavedataFileData(); read(icon0); titleAddr = read32(); if (titleAddr != 0) { title = Utilities.readStringZ(mem, titleAddr); } else { title = null; } } @Override protected void write() { write(icon0); write32(titleAddr); if (titleAddr != 0) { Utilities.writeStringZ(mem, titleAddr, title); } } @Override public int sizeof() { return icon0.sizeof() + 4; } } public static class PspUtilitySavedataSecureFile { public static final int SIZEOF = 32; private static final int FILENAME_LENGTH = 13; public String fileName; public byte[] key = new byte[0x10]; public PspUtilitySavedataSecureFile() { } public PspUtilitySavedataSecureFile(String fileName, byte[] key) { this.fileName = fileName; if (key != null) { System.arraycopy(key, 0, this.key, 0, this.key.length); } } public void write(byte[] buffer, int offset) { byte[] fileNameBytes = fileName.getBytes(); System.arraycopy(fileNameBytes, 0, buffer, offset, fileNameBytes.length); if (fileNameBytes.length < FILENAME_LENGTH) { Arrays.fill(buffer, offset + fileNameBytes.length, offset + FILENAME_LENGTH, (byte) 0); } System.arraycopy(key, 0, buffer, offset + FILENAME_LENGTH, key.length); } public boolean read(byte[] buffer, int offset) { if (offset + PspUtilitySavedataSecureFile.SIZEOF > buffer.length) { return false; } int fileNameLength = FILENAME_LENGTH; while (fileNameLength > 0) { if (buffer[offset + fileNameLength - 1] != (byte) 0) { break; } fileNameLength--; } if (fileNameLength <= 0) { return false; } fileName = new String(buffer, offset, fileNameLength); System.arraycopy(buffer, offset + FILENAME_LENGTH, key, 0, key.length); return true; } } public static class PspUtilitySavedataSecureFileList { public static final int NUMBER_FILES = 99; public static final int SIZEOF = NUMBER_FILES * PspUtilitySavedataSecureFile.SIZEOF; public List<PspUtilitySavedataSecureFile> fileList = new LinkedList<SceUtilitySavedataParam.PspUtilitySavedataSecureFile>(); public byte[] getBytes() { byte[] bytes = new byte[SIZEOF]; int offset = 0; for (PspUtilitySavedataSecureFile file : fileList) { file.write(bytes, offset); offset += PspUtilitySavedataSecureFile.SIZEOF; } return bytes; } public void read(byte[] buffer) { fileList.clear(); for (int offset = 0; offset < PspUtilitySavedataSecureFileList.SIZEOF; offset += PspUtilitySavedataSecureFile.SIZEOF) { PspUtilitySavedataSecureFile file = new PspUtilitySavedataSecureFile(); if (!file.read(buffer, offset)) { break; } fileList.add(file); } } public boolean contains(String fileName) { for (PspUtilitySavedataSecureFile file : fileList) { if (file.fileName.equals(fileName)) { return true; } } return false; } public void add(String fileName, byte[] key) { if (contains(fileName)) { return; } PspUtilitySavedataSecureFile file = new PspUtilitySavedataSecureFile(fileName, key); fileList.add(file); } public void update(String fileName, byte[] key) { boolean found = false; for (PspUtilitySavedataSecureFile file : fileList) { if (file.fileName.equals(fileName)) { file.key = key; found = true; } } if (!found) { PspUtilitySavedataSecureFile file = new PspUtilitySavedataSecureFile(fileName, key); fileList.add(file); } } } @Override protected void read() { base = new pspUtilityDialogCommon(); read(base); setMaxSize(base.totalSizeof()); mode = read32(); // Offset 48 bind = read32(); // Offset 52 overwrite = read32() == 0 ? false : true; // Offset 56 gameName = readStringNZ(13); // Offset 60 readUnknown(3); saveName = readStringNZ(20); // Offset 76 saveNameListAddr = read32(); // Offset 96 if (Memory.isAddressGood(saveNameListAddr)) { List<String> newSaveNameList = new ArrayList<String>(); boolean endOfList = false; for (int i = 0; !endOfList; i += 20) { String saveNameItem = Utilities.readStringNZ(mem, saveNameListAddr + i, 20); if (saveNameItem == null || saveNameItem.length() == 0) { endOfList = true; } else { newSaveNameList.add(saveNameItem); } } saveNameList = newSaveNameList.toArray(new String[newSaveNameList.size()]); } fileName = readStringNZ(13); // Offset 100 readUnknown(3); dataBuf = read32(); // Offset 116 dataBufSize = read32(); // Offset 120 dataSize = read32(); // Offset 124 sfoParam = new PspUtilitySavedataSFOParam(); read(sfoParam); // Offset 128 icon0FileData = new PspUtilitySavedataFileData(); read(icon0FileData); // Offset 1412 icon1FileData = new PspUtilitySavedataFileData(); read(icon1FileData); // Offset 1428 pic1FileData = new PspUtilitySavedataFileData(); read(pic1FileData); // Offset 1444 snd0FileData = new PspUtilitySavedataFileData(); read(snd0FileData); // Offset 1460 newDataAddr = read32(); // Offset 1476 if (newDataAddr != 0) { newData = new PspUtilitySavedataListSaveNewData(); newData.read(mem, newDataAddr); } else { newData = null; } focus = read32(); // Offset 1480 abortStatus = read32(); // Offset 1484 msFreeAddr = read32(); // Offset 1488 msDataAddr = read32(); // Offset 1492 utilityDataAddr = read32(); // Offset 1496 read8Array(key); // Offset 1500 secureVersion = read32(); // Offset 1516 multiStatus = read32(); // Offset 1520 idListAddr = read32(); // Offset 1524 fileListAddr = read32(); // Offset 1528 sizeAddr = read32(); // Offset 1532 } @Override protected void write() { write(base); setMaxSize(base.totalSizeof()); write32(mode); write32(bind); write32(overwrite ? 1 : 0); writeStringNZ(13, gameName); writeUnknown(3); writeStringNZ(20, saveName); write32(saveNameListAddr); writeStringNZ(13, fileName); writeUnknown(3); write32(dataBuf); write32(dataBufSize); write32(dataSize); write(sfoParam); write(icon0FileData); write(icon1FileData); write(pic1FileData); write(snd0FileData); write32(newDataAddr); if (newDataAddr != 0) { newData.write(mem, newDataAddr); } write32(focus); write32(abortStatus); write32(msFreeAddr); write32(msDataAddr); write32(utilityDataAddr); write8Array(key); write32(secureVersion); write32(multiStatus); write32(idListAddr); write32(fileListAddr); write32(sizeAddr); } public String getBasePath() { return getBasePath(gameName, saveName); } public String getBasePath(String saveName) { return getBasePath(gameName, saveName); } public String getBasePath(String gameName, String saveName) { String path = savedataPath + gameName; if (saveName != null && !anyFileName.equals(saveName)) { path += saveName; } path += "/"; return path; } public String getFileName(String saveName, String fileName) { return getFileName(gameName, saveName, fileName); } public String getFileName(String gameName, String saveName, String fileName) { return getBasePath(gameName, saveName) + fileName; } private static int computeMemoryStickRequiredSpaceKb(int sizeByte) { int sizeKb = Utilities.getSizeKb(sizeByte); int sectorSizeKb = MemoryStick.getSectorSizeKb(); int numberSectors = (sizeKb + sectorSizeKb - 1) / sectorSizeKb; return numberSectors * sectorSizeKb; } public int getRequiredSizeKb() { int requiredSpaceKb = 0; requiredSpaceKb += MemoryStick.getSectorSizeKb(); // Assume 1 sector for SFO-Params // Add the dataSize only if a fileName has been provided if (fileName != null && fileName.length() > 0) { requiredSpaceKb += computeMemoryStickRequiredSpaceKb(dataSize + 15); } requiredSpaceKb += computeMemoryStickRequiredSpaceKb(icon0FileData.size); requiredSpaceKb += computeMemoryStickRequiredSpaceKb(icon1FileData.size); requiredSpaceKb += computeMemoryStickRequiredSpaceKb(pic1FileData.size); requiredSpaceKb += computeMemoryStickRequiredSpaceKb(snd0FileData.size); return requiredSpaceKb; } public int getSizeKb(String gameName, String saveName) { int sizeKb = 0; String path = getBasePath(gameName, saveName); StringBuilder localFileName = new StringBuilder(); IVirtualFileSystem vfs = Modules.IoFileMgrForUserModule.getVirtualFileSystem(path, localFileName); if (vfs != null) { String[] fileNames = vfs.ioDopen(localFileName.toString()); if (fileNames != null) { for (int i = 0; i < fileNames.length; i++) { SceIoStat stat = new SceIoStat(); SceIoDirent dirent = new SceIoDirent(stat, fileNames[i]); int result = vfs.ioDread(localFileName.toString(), dirent); if (result > 0) { sizeKb += Utilities.getSizeKb((int) stat.size); } } vfs.ioDclose(localFileName.toString()); } } return sizeKb; } private SeekableDataInput getDataInput(String path, String name) { SeekableDataInput fileInput = Modules.IoFileMgrForUserModule.getFile(path + name, jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_RDONLY); return fileInput; } private SeekableRandomFile getDataOutput(String path, String name) { SeekableDataInput fileInput = Modules.IoFileMgrForUserModule.getFile(path + name, jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_RDWR | jpcsp.HLE.modules.IoFileMgrForUser.PSP_O_CREAT); if (fileInput instanceof SeekableRandomFile) { return (SeekableRandomFile) fileInput; } return null; } public boolean deleteDir(String path) { return Modules.IoFileMgrForUserModule.rmdir(path, true); } public boolean deleteFile(String filename) { boolean success = false; if (filename != null && filename.length() > 0) { File f = new File(getBasePath().replace(":", "/") + filename); success = f.delete(); } return success; } public void load(Memory mem) throws IOException { String path = getBasePath(); // Read the main data. // The data has to be decrypted if the SFO is marked for encryption and // the file is listed in the SFO as a secure file (SAVEDATA_FILE_LIST). if (checkParamSFOEncryption(path, paramSfoFileName) && isSecureFile(fileName)) { dataSize = loadEncryptedFile(mem, path, fileName, dataBuf, dataBufSize, key); } else { dataSize = loadFile(mem, path, fileName, dataBuf, dataBufSize); } // Read ICON0.PNG safeLoad(mem, icon0FileName, icon0FileData); // Check and read ICON1.PMF or ICON1.PNG if (icon1FileData.buf == 0) { icon1FileData.size = 0; } else { String icon1FileName; if (mem.read8(icon1FileData.buf) != 0x89) { icon1FileName = icon1PMFFileName; } else { icon1FileName = icon1PNGFileName; } safeLoad(mem, icon1FileName, icon1FileData); } // Read PIC1.PNG safeLoad(mem, pic1FileName, pic1FileData); // Read SND0.AT3 safeLoad(mem, snd0FileName, snd0FileData); // Read PARAM.SFO loadPsf(mem, path, paramSfoFileName, sfoParam); bind = BIND_IS_OK; abortStatus = 0; } private void safeLoad(Memory mem, String filename, PspUtilitySavedataFileData fileData) throws IOException { String path = getBasePath(); try { fileData.size = loadFile(mem, path, filename, fileData.buf, fileData.bufSize); } catch (FileNotFoundException e) { // ignore } } public void save(Memory mem) throws IOException { save(mem, false); } public void save(Memory mem, boolean secure) throws IOException { String path = getBasePath(); Modules.IoFileMgrForUserModule.mkdirs(path); // Copy the original SAVEDATA key. byte[] sdkey = key; // Write main data. if (CryptoEngine.getSavedataCryptoStatus() && secure) { if (CryptoEngine.getExtractSavedataKeyStatus()) { String tmpPath = Settings.getInstance().getDiscTmpDirectory(); new File(tmpPath).mkdirs(); SeekableRandomFile keyFileOutput = new SeekableRandomFile(tmpPath + "SDKEY.bin", "rw"); keyFileOutput.write(sdkey, 0, sdkey.length); keyFileOutput.close(); } writeEncryptedFile(mem, path, fileName, dataBuf, dataSize, key); } else { writeFile(mem, path, fileName, dataBuf, dataSize); } // Write ICON0.PNG writePNG(mem, path, icon0FileName, icon0FileData.buf, icon0FileData.size); // Check and write ICON1.PMF or ICON1.PNG IMemoryReader memoryReader = MemoryReader.getMemoryReader(icon1FileData.buf, 1); String icon1FileName; if ((byte) memoryReader.readNext() != (byte) 0x89) { icon1FileName = icon1PMFFileName; } else { icon1FileName = icon1PNGFileName; } writePNG(mem, path, icon1FileName, icon1FileData.buf, icon1FileData.size); // Write PIC1.PNG writePNG(mem, path, pic1FileName, pic1FileData.buf, pic1FileData.size); // Write SND0.AT3 writeFile(mem, path, snd0FileName, snd0FileData.buf, snd0FileData.size); // Write PARAM.SFO writePsf(mem, path, paramSfoFileName, sfoParam, CryptoEngine.getSavedataCryptoStatus(), fileName, sdkey, key); } private int loadFile(Memory mem, String path, String name, int address, int maxLength) throws IOException { if (name == null || name.length() <= 0) { return 0; } int fileSize = 0; SeekableDataInput fileInput = null; try { fileInput = getDataInput(path, name); if (fileInput == null) { throw new FileNotFoundException("File not found '" + path + "' '" + name + "'"); } // Some applications set dataBufSize to -1 on purpose. The reason behind this // is still unknown, but, for these cases, ignore maxLength. fileSize = (int) fileInput.length(); if (fileSize > maxLength && maxLength > 0) { fileSize = maxLength; } else if (address == 0) { fileSize = 0; } Utilities.readFully(fileInput, address, fileSize); } finally { if (fileInput != null) { fileInput.close(); } } return fileSize; } private void writeEncryptedFile(Memory mem, String path, String name, int address, int length, byte[] key) throws IOException { if (name == null || name.length() <= 0 || address == 0) { return; } SeekableRandomFile fileOutput = null; try { fileOutput = getDataOutput(path, name); if (fileOutput != null) { byte[] inBuf = new byte[length + 0x10]; IMemoryReader memoryReader = MemoryReader.getMemoryReader(address, 1); for (int i = 0; i < length; i++) { inBuf[i] = (byte) memoryReader.readNext(); } // Replace the key with the generated hash. CryptoEngine crypto = new CryptoEngine(); this.key = crypto.getSAVEDATAEngine().EncryptSavedata(inBuf, length, key); fileOutput.getChannel().truncate(inBuf.length); // Avoid writing leftover bytes from previous encryption. fileOutput.write(inBuf, 0, inBuf.length); } } finally { if (fileOutput != null) { fileOutput.close(); } } } private int loadEncryptedFile(Memory mem, String path, String name, int address, int maxLength, byte[] key) throws IOException { if (name == null || name.length() <= 0 || address == 0) { return 0; } int length = 0; SeekableDataInput fileInput = null; try { fileInput = getDataInput(path, name); if (fileInput == null) { throw new FileNotFoundException("File not found '" + path + "' '" + name + "'"); } int fileSize = (int) fileInput.length(); byte[] inBuf = new byte[fileSize]; fileInput.readFully(inBuf); CryptoEngine crypto = new CryptoEngine(); byte[] outBuf = crypto.getSAVEDATAEngine().DecryptSavedata(inBuf, fileSize, key); IMemoryWriter memoryWriter = MemoryWriter.getMemoryWriter(address, 1); length = Math.min(outBuf.length, maxLength); for (int i = 0; i < length; i++) { memoryWriter.writeNext(outBuf[i]); } memoryWriter.flush(); } finally { if (fileInput != null) { fileInput.close(); } } return length; } private void writeFile(Memory mem, String path, String name, int address, int length) throws IOException { if (name == null || name.length() <= 0 || address == 0) { return; } SeekableRandomFile fileOutput = null; try { fileOutput = getDataOutput(path, name); if (fileOutput != null) { fileOutput.getChannel().truncate(length); // Avoid writing leftover bytes from previous encryption. Utilities.write(fileOutput, address, length); } } finally { if (fileOutput != null) { fileOutput.close(); } } } private void writePNG(Memory mem, String path, String name, int address, int length) throws IOException { // The PSP is saving only the real size of the PNG file, // which could be smaller than the buffer size length = PNG.getEndOfPNG(mem, address, length); writeFile(mem, path, name, address, length); } private boolean checkParamSFOEncryption(String path, String name) throws IOException { boolean isEncrypted = false; SeekableDataInput fileInput = null; try { fileInput = getDataInput(path, name); if (fileInput != null && fileInput.length() > 0) { byte[] buffer = new byte[(int) fileInput.length()]; fileInput.readFully(buffer); fileInput.close(); // SAVEDATA PARAM.SFO has a fixed size of 0x1330 bytes. // In order to determine if the SAVEDATA is encrypted or not, // we must check if the check bit at 0x11B0 is set (an identical check // is performed on a real PSP). if (buffer.length == 0x1330) { if (buffer[0x11B0] != 0) { isEncrypted = true; } } } } finally { if (fileInput != null) { fileInput.close(); } } return isEncrypted; } private PSF readPsf(String path, String name) throws IOException { PSF psf = null; SeekableDataInput fileInput = null; try { fileInput = getDataInput(path, name); if (fileInput != null && fileInput.length() > 0) { byte[] buffer = new byte[(int) fileInput.length()]; fileInput.readFully(buffer); fileInput.close(); psf = new PSF(); psf.read(ByteBuffer.wrap(buffer)); } } finally { if (fileInput != null) { fileInput.close(); } } return psf; } private void loadPsf(Memory mem, String path, String name, PspUtilitySavedataSFOParam sfoParam) throws IOException { PSF psf = readPsf(path, name); if (psf != null) { sfoParam.parentalLevel = psf.getNumeric("PARENTAL_LEVEL"); sfoParam.title = psf.getString("TITLE"); sfoParam.detail = psf.getString("SAVEDATA_DETAIL"); sfoParam.savedataTitle = psf.getString("SAVEDATA_TITLE"); } } private void writePsf(Memory mem, String path, String psfName, PspUtilitySavedataSFOParam sfoParam, boolean cryptoMode, String dataName, byte[] key, byte[] hash) throws IOException { SeekableRandomFile psfOutput = null; try { psfOutput = getDataOutput(path, psfName); if (psfOutput == null) { return; } // Generate different PSF instances for plain PSF and encrypted PSF (with hashes). PSF psf = new PSF(); PSF encryptedPsf = new PSF(); // Test if a PARAM.SFO already exists and save it's SAVEDATA_PARAMS. byte[] savedata_params_old = new byte[128]; PSF oldPsf = readPsf(path, psfName); if (oldPsf != null) { savedata_params_old = (byte[]) oldPsf.get("SAVEDATA_PARAMS"); } // Insert CATEGORY. psf.put("CATEGORY", "MS", 4); encryptedPsf.put("CATEGORY", "MS", 4); // Insert PARENTAL_LEVEL. psf.put("PARENTAL_LEVEL", sfoParam.parentalLevel); encryptedPsf.put("PARENTAL_LEVEL", sfoParam.parentalLevel); // Insert SAVEDATA_DETAIL. psf.put("SAVEDATA_DETAIL", sfoParam.detail, 1024); encryptedPsf.put("SAVEDATA_DETAIL", sfoParam.detail, 1024); // Insert SAVEDATA_DIRECTORY. if (saveName.equals("<>")) { // Do not write the saveName if it's "<>". psf.put("SAVEDATA_DIRECTORY", gameName, 64); encryptedPsf.put("SAVEDATA_DIRECTORY", gameName, 64); } else { psf.put("SAVEDATA_DIRECTORY", gameName + saveName, 64); encryptedPsf.put("SAVEDATA_DIRECTORY", gameName + saveName, 64); } // Insert SAVEDATA_FILE_LIST. PspUtilitySavedataSecureFileList secureFileList = getSecureFileList(null); // Even if the main data file is not being saved by a secure method, if the // hash is not null then the file is saved in the file list. if (hash != null) { // Add the current dataName as a secure file name if (secureFileList == null) { secureFileList = new PspUtilitySavedataSecureFileList(); } // Only add the file hash if using encryption. if (cryptoMode) { secureFileList.update(dataName, hash); } else { byte[] clearHash = new byte[0x10]; secureFileList.update(dataName, clearHash); } } if (secureFileList != null) { psf.put("SAVEDATA_FILE_LIST", secureFileList.getBytes()); encryptedPsf.put("SAVEDATA_FILE_LIST", secureFileList.getBytes()); } // Generate blank SAVEDATA_PARAMS for plain PSF. byte[] savedata_params = new byte[128]; psf.put("SAVEDATA_PARAMS", savedata_params); // Insert the remaining params for plain PSF. psf.put("SAVEDATA_TITLE", sfoParam.savedataTitle, 128); psf.put("TITLE", sfoParam.title, 128); // Setup a temporary buffer for encryption (PARAM.SFO size is 0x1330). ByteBuffer buf = ByteBuffer.allocate(0x1330); // Save back the PARAM.SFO data to be encrypted. psf.write(buf); // Generate a new PARAM.SFO and update file hashes. if (cryptoMode) { CryptoEngine crypto = new CryptoEngine(); int sfoSize = buf.array().length; byte[] sfoData = buf.array(); // Generate the final SAVEDATA_PARAMS (encrypted). crypto.getSAVEDATAEngine().UpdateSavedataHashes(encryptedPsf, sfoData, sfoSize, savedata_params_old, key); // Insert the remaining params for encrypted PSF. encryptedPsf.put("SAVEDATA_TITLE", sfoParam.savedataTitle, 128); encryptedPsf.put("TITLE", sfoParam.title, 128); // Write the new encrypted PARAM.SFO (with hashes) from the encrypted PSF. encryptedPsf.write(psfOutput); } else { // Write the new PARAM.SFO (without hashes) from the plain PSF. psf.write(psfOutput); } } finally { if (psfOutput != null) { // Close the PARAM.SFO file stream. psfOutput.close(); } } } public boolean test(Memory mem) throws IOException { String path = getBasePath(); boolean result = testFile(mem, path, fileName); return result; } private boolean testFile(Memory mem, String path, String name) throws IOException { if (name == null || name.length() <= 0) { return false; } SeekableDataInput fileInput = null; try { fileInput = getDataInput(path, name); if (fileInput == null) { throw new FileNotFoundException("File not found '" + path + "' '" + name + "'"); } } finally { if (fileInput != null) { fileInput.close(); } } return true; } public String getAnySaveName(String gameName, String saveName) { // NULL can also be sent in saveName (seen in MODE_SIZES). // It means any save from the current game, since all saves share a common // save data file. if (saveName == null || saveName.length() <= 0 || anyFileName.equals(saveName)) { File f = new File(savedataFilePath); String[] entries = f.list(); if (entries != null) { for (int i = 0; i < f.list().length; i++) { if (entries[i].startsWith(gameName)) { saveName = entries[i].replace(gameName, ""); break; } } } } return saveName; } public boolean isDirectoryPresent(String gameName, String saveName) { saveName = getAnySaveName(gameName, saveName); String path = getBasePath(gameName, saveName); SceIoStat stat = Modules.IoFileMgrForUserModule.statFile(path); if (stat != null && (stat.attr & 0x20) == 0) { return true; } return false; } public boolean isPresent(String gameName, String saveName) { saveName = getAnySaveName(gameName, saveName); // When NULL is sent in fileName, it means any file inside the savedata folder. if (fileName == null || fileName.length() <= 0) { File f = new File(savedataFilePath + gameName + saveName); if (f.list() == null) { return false; } return true; } String path = getBasePath(gameName, saveName); try { SeekableDataInput fileInput = getDataInput(path, fileName); if (fileInput != null) { fileInput.close(); return true; } } catch (IOException e) { } return false; } public boolean isPresent() { return isPresent(gameName, saveName); } public boolean isGameDirectoryPresent() { String path = getBasePath(); SceIoStat gameDirectoryStat = Modules.IoFileMgrForUserModule.statFile(path); return gameDirectoryStat != null; } public long getTimestamp(String gameName, String saveName) { String sfoFileName = getFileName(gameName, saveName, paramSfoFileName); SceIoStat sfoStat = Modules.IoFileMgrForUserModule.statFile(sfoFileName); if (sfoStat != null) { Calendar cal = Calendar.getInstance(); ScePspDateTime pspTime = sfoStat.mtime; cal.set(pspTime.year, pspTime.month, pspTime.day, pspTime.hour, pspTime.minute, pspTime.second); return cal.getTimeInMillis(); } return 0; } public Calendar getSavedTime() { return getSavedTime(saveName); } public Calendar getSavedTime(String saveName) { String sfoFileName = getFileName(saveName, SceUtilitySavedataParam.paramSfoFileName); SceIoStat sfoStat = Modules.IoFileMgrForUserModule.statFile(sfoFileName); if (sfoStat == null) { return null; } ScePspDateTime pspTime = sfoStat.mtime; Calendar savedTime = Calendar.getInstance(); // pspTime.month has a value in range [1..12], Calendar requires a value in range [0..11] savedTime.set(pspTime.year, pspTime.month - 1, pspTime.day, pspTime.hour, pspTime.minute, pspTime.second); return savedTime; } @Override public int sizeof() { return base.totalSizeof(); } public String getModeName() { if (mode < 0 || mode >= modeNames.length) { return String.format("UNKNOWN_MODE%d", mode); } return modeNames[mode]; } public static boolean isSystemFile(String fileName) { for (int i = 0; i < systemFileNames.length; i++) { if (systemFileNames[i].equalsIgnoreCase(fileName)) { return true; } } return false; } private PspUtilitySavedataSecureFileList getSecureFileList(String fileName) { PSF psf = null; try { psf = readPsf(getBasePath(), paramSfoFileName); } catch (IOException e) { } if (psf == null) { return null; } Object savedataFileList = psf.get("SAVEDATA_FILE_LIST"); if (savedataFileList == null || !(savedataFileList instanceof byte[])) { return null; } PspUtilitySavedataSecureFileList fileList = null; if (savedataFileList != null) { fileList = new PspUtilitySavedataSecureFileList(); fileList.read((byte[]) savedataFileList); } return fileList; } public boolean isSecureFile(String fileName) { PspUtilitySavedataSecureFileList fileList = getSecureFileList(fileName); if (fileList == null) { return false; } return fileList.contains(fileName); } @Override public String toString() { StringBuilder s = new StringBuilder(); s.append(String.format("Address 0x%08X, mode=%d(%s), gameName=%s, saveName=%s, fileName=%s, secureVersion=%d", getBaseAddress(), mode, getModeName(), gameName, saveName, fileName, secureVersion)); for (int i = 0; saveNameList != null && i < saveNameList.length; i++) { if (i == 0) { s.append(", saveNameList=["); } else { s.append(", "); } s.append(saveNameList[i]); if (i == saveNameList.length - 1) { s.append("]"); } } return s.toString(); } }