/*
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.format;
import static jpcsp.util.Utilities.readStringZ;
import static jpcsp.util.Utilities.readUByte;
import static jpcsp.util.Utilities.readUHalf;
import static jpcsp.util.Utilities.readUWord;
import static jpcsp.util.Utilities.writeByte;
import static jpcsp.util.Utilities.writeHalf;
import static jpcsp.util.Utilities.writeStringZ;
import static jpcsp.util.Utilities.writeWord;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import jpcsp.util.Constants;
public class PSF {
private int psfOffset;
private int size;
private boolean sizeDirty;
private boolean tablesDirty;
private int ident;
private int version; // yapspd: 0x1100. actual: 0x0101.
private int keyTableOffset;
private int valueTableOffset;
private int indexEntryCount;
private LinkedList<PSFKeyValuePair> pairList;
public final static int PSF_IDENT = 0x46535000;
public final static int PSF_DATA_TYPE_BINARY = 0;
public final static int PSF_DATA_TYPE_STRING = 2;
public final static int PSF_DATA_TYPE_INT32 = 4;
public PSF(int psfOffset) {
this.psfOffset = psfOffset;
size = 0;
sizeDirty = true;
tablesDirty = true;
ident = PSF_IDENT;
version = 0x0101;
pairList = new LinkedList<PSFKeyValuePair>();
}
public PSF() {
this(0);
}
/** f.position() is undefined after calling this */
public void read(ByteBuffer f) throws IOException {
psfOffset = f.position();
ident = readUWord(f);
if (ident != PSF_IDENT) {
System.out.println("Not a valid PSF file (ident=" + String.format("%08X", ident) + ")");
return;
}
// header
version = readUWord(f); // 0x0101
keyTableOffset = readUWord(f);
valueTableOffset = readUWord(f);
indexEntryCount = readUWord(f);
// index table
for (int i = 0; i < indexEntryCount; i++) {
PSFKeyValuePair pair = new PSFKeyValuePair();
pair.read(f);
pairList.add(pair);
}
// key/pairs
for (PSFKeyValuePair pair : pairList) {
f.position(psfOffset + keyTableOffset + pair.keyOffset);
pair.key = readStringZ(f);
f.position(psfOffset + valueTableOffset + pair.valueOffset);
switch(pair.dataType) {
case PSF_DATA_TYPE_BINARY:
byte[] data = new byte[pair.dataSize];
f.get(data);
pair.data = data;
//System.out.println(String.format("offset=%08X key='%s' binary packed [len=%d]",
// keyTableOffset + pair.keyOffset, pair.key, pair.dataSize));
break;
case PSF_DATA_TYPE_STRING:
// String may not be in english!
byte[] s = new byte[pair.dataSize];
f.get(s);
// Strip trailing null character
pair.data = new String(s, 0, s[s.length - 1] == '\0' ? s.length - 1 : s.length, Constants.charset);
//System.out.println(String.format("offset=%08X key='%s' string '%s' [len=%d]",
// keyTableOffset + pair.keyOffset, pair.key, pair.data, pair.dataSize));
break;
case PSF_DATA_TYPE_INT32:
pair.data = readUWord(f);
//System.out.println(String.format("offset=%08X key='%s' int32 %08X %d [len=%d]",
// keyTableOffset + pair.keyOffset, pair.key, pair.data, pair.data, pair.dataSize));
break;
default:
System.out.println(String.format("offset=%08X key='%s' unhandled data type %d [len=%d]",
keyTableOffset + pair.keyOffset, pair.key, pair.dataType, pair.dataSize));
break;
}
}
sizeDirty = true;
tablesDirty = false;
calculateSize();
}
public void write(RandomAccessFile output) throws IOException {
byte[] buffer = new byte[size()];
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
write(byteBuffer);
// Write the file and truncate it to the correct length
output.write(buffer);
output.setLength(buffer.length);
}
// assumes we want to write at the start of the buffer, and that the current buffer position is 0
// doesn't handle psfOffset
public void write(ByteBuffer f) {
if (indexEntryCount != pairList.size())
throw new RuntimeException("incremental size and actual size do not match! " + indexEntryCount + "/" + pairList.size());
if (tablesDirty) {
calculateTables();
}
// header
writeWord(f, ident);
writeWord(f, version);
writeWord(f, keyTableOffset);
writeWord(f, valueTableOffset);
writeWord(f, indexEntryCount);
// index table
for (PSFKeyValuePair pair : pairList) {
pair.write(f);
}
// key/value pairs
for (PSFKeyValuePair pair : pairList) {
f.position(keyTableOffset + pair.keyOffset);
//System.err.println("PSF write key fp=" + f.position() + " datalen=" + (pair.key.length() + 1) + " top=" + (f.position() + pair.key.length() + 1));
writeStringZ(f, pair.key);
f.position(valueTableOffset + pair.valueOffset);
//System.err.println("PSF write value fp=" + f.position() + " datalen=" + (pair.dataSizePadded) + " top=" + (f.position() + pair.dataSizePadded));
switch(pair.dataType) {
case PSF_DATA_TYPE_BINARY:
f.put((byte[])pair.data);
break;
case PSF_DATA_TYPE_STRING:
String s = (String)pair.data;
f.put(s.getBytes(Constants.charset));
writeByte(f, (byte)0);
break;
case PSF_DATA_TYPE_INT32:
writeWord(f, (Integer)pair.data);
break;
default:
System.out.println("not writing unhandled data type " + pair.dataType);
break;
}
}
}
public Object get(String key) {
for (PSFKeyValuePair pair : pairList) {
if (pair.key.equals(key))
return pair.data;
}
return null;
}
public String getString(String key) {
Object obj = get(key);
if (obj != null)
return (String)obj;
return null;
}
/** kxploit patcher tool adds "\nKXPloit Boot by PSP-DEV Team" */
public String getPrintableString(String key) {
String rawString = getString(key);
if (rawString == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rawString.length(); i++) {
char c = rawString.charAt(i);
if (c == '\0' || c == '\n')
break;
sb.append(rawString.charAt(i));
}
return sb.toString();
}
public int getNumeric(String key) {
Object obj = get(key);
if (obj != null)
return (Integer)obj;
return 0;
}
public void put(String key, byte[] data) {
PSFKeyValuePair pair = new PSFKeyValuePair(key, PSF_DATA_TYPE_BINARY, data.length, data);
pairList.add(pair);
sizeDirty = true;
tablesDirty = true;
indexEntryCount++;
}
public void put(String key, String data, int rawlen) {
byte[] b = (data.getBytes(Constants.charset));
//if (b.length != data.length())
// System.out.println("put string '" + data + "' size mismatch. UTF-8=" + b.length + " regular=" + (data.length() + 1));
//PSFKeyValuePair pair = new PSFKeyValuePair(key, PSF_DATA_TYPE_STRING, data.length() + 1, rawlen, data);
PSFKeyValuePair pair = new PSFKeyValuePair(key, PSF_DATA_TYPE_STRING, b.length + 1, rawlen, data);
pairList.add(pair);
sizeDirty = true;
tablesDirty = true;
indexEntryCount++;
}
public void put(String key, String data) {
byte[] b = (data.getBytes(Constants.charset));
//int rawlen = data.length() + 1;
int rawlen = b.length + 1;
put(key, data, (rawlen + 3) & ~3);
}
public void put(String key, int data) {
PSFKeyValuePair pair = new PSFKeyValuePair(key, PSF_DATA_TYPE_INT32, 4, data);
pairList.add(pair);
sizeDirty = true;
tablesDirty = true;
indexEntryCount++;
}
private void calculateTables() {
tablesDirty = false;
// position the key table after the index table and before the value table
// 20 byte PSF header
// 16 byte per index entry
keyTableOffset = 5 * 4 + indexEntryCount * 0x10;
// position the value table after the key table
valueTableOffset = keyTableOffset;
for (PSFKeyValuePair pair : pairList) {
// keys are not aligned
valueTableOffset += pair.key.length() + 1;
}
// 32-bit align for data start
valueTableOffset = (valueTableOffset + 3) & ~3;
// index table
int keyRunningOffset = 0;
int valueRunningOffset = 0;
for (PSFKeyValuePair pair : pairList) {
pair.keyOffset = keyRunningOffset;
keyRunningOffset += pair.key.length() + 1;
pair.valueOffset = valueRunningOffset;
valueRunningOffset += pair.dataSizePadded;
}
}
private void calculateSize() {
sizeDirty = false;
size = 0;
if (tablesDirty) {
calculateTables();
}
for (PSFKeyValuePair pair : pairList) {
int keyHighBound = keyTableOffset + pair.keyOffset + pair.key.length() + 1;
int valueHighBound = valueTableOffset + pair.valueOffset + pair.dataSizePadded;
if (keyHighBound > size)
size = keyHighBound;
if (valueHighBound > size)
size = valueHighBound;
}
}
public int size() {
if (sizeDirty) {
calculateSize();
}
return size;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (PSFKeyValuePair pair : pairList) {
if (sb.length() > 0) {
sb.append(System.lineSeparator());
}
sb.append(pair.toString());
}
if (isLikelyHomebrew()) {
if (sb.length() > 0) {
sb.append(System.lineSeparator());
}
sb.append("This is likely a homebrew");
}
return sb.toString();
}
/** used by isLikelyHomebrew() */
private boolean safeEquals(Object a, Object b) {
return (a == null && b == null) || (a != null && a.equals(b));
}
public boolean isLikelyHomebrew() {
boolean homebrew = false;
String disc_version = getString("DISC_VERSION");
String disc_id = getString("DISC_ID");
String category = getString("CATEGORY");
Integer bootable = (Integer)get("BOOTABLE"); // don't use getNumeric, we also want to know if the entry exists or not
Integer region = (Integer)get("REGION");
String psp_system_ver = getString("PSP_SYSTEM_VER");
Integer parental_level = (Integer)get("PARENTAL_LEVEL");
Integer ref_one = new Integer(1);
Integer ref_region = new Integer(32768);
if (safeEquals(disc_version, "1.00") &&
safeEquals(disc_id, "UCJS10041") && // loco roco demo, should not false positive since that demo has sys ver 3.40
safeEquals(category, "MG") &&
safeEquals(bootable, ref_one) &&
safeEquals(region, ref_region) &&
safeEquals(psp_system_ver, "1.00") &&
safeEquals(parental_level, ref_one)) {
if (indexEntryCount == 8) {
homebrew = true;
} else if (indexEntryCount == 9 &&
safeEquals(get("MEMSIZE"), ref_one)) {
// lua player hm 8
homebrew = true;
}
} else if (indexEntryCount == 4 &&
safeEquals(category, "MG") &&
safeEquals(bootable, ref_one) &&
safeEquals(region, ref_region)) {
homebrew = true;
}
return homebrew;
}
public static class PSFKeyValuePair {
// index table info
public int keyOffset;
public int unknown1;
public int dataType;
public int dataSize;
public int dataSizePadded;
public int valueOffset;
// key table info
public String key;
// data table info
public Object data;
public PSFKeyValuePair() {
this(null, 0, 0, null);
}
public PSFKeyValuePair(String key, int dataType, int dataSize, Object data) {
this(key, dataType, dataSize, (dataSize + 3) & ~3, data);
}
public PSFKeyValuePair(String key, int dataType, int dataSize, int dataSizePadded, Object data) {
this.key = key;
this.dataType = dataType;
this.dataSize = dataSize;
this.dataSizePadded = dataSizePadded;
this.data = data;
// yapspd: 4
// probably alignment of the value data
unknown1 = 4;
}
/** only reads the index entry, since this class has doesn't know about the psf/key/value offsets */
public void read(ByteBuffer f) throws IOException {
// index table entry
keyOffset = readUHalf(f);
unknown1 = readUByte(f);
dataType = readUByte(f);
dataSize = readUWord(f);
dataSizePadded = readUWord(f);
valueOffset = readUWord(f);
}
/** only writes the index entry, since this class has doesn't know about the psf/key/value offsets */
public void write(ByteBuffer f) {
// index table entry
writeHalf(f, keyOffset);
writeByte(f, unknown1);
writeByte(f, dataType);
writeWord(f, dataSize);
writeWord(f, dataSizePadded);
writeWord(f, valueOffset);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
/*
sb.append("index entry:\n");
sb.append(String.format("keyOffset 0x%08X %d\n", keyOffset, keyOffset));
sb.append(String.format("unknown1 0x%08X %d\n", unknown1, unknown1));
sb.append(String.format("dataType 0x%08X %d\n", dataType, dataType));
sb.append(String.format("dataSize 0x%08X %d\n", dataSize, dataSize));
sb.append(String.format("dataSizePadding 0x%08X %d\n", dataSizePadding, dataSizePadding));
sb.append(String.format("valueOffset 0x%08X %d\n", valueOffset, valueOffset));
*/
//sb.append(String.format("[offset=%08X] '%s' = [offset=%08X,len=%d,rawlen=%d] '" + data + "'",
// keyOffset, key, valueOffset, dataSize, dataSizePadded));
sb.append(key + " = " + data);
return sb.toString();
}
}
}