2016-02-02 19:38:53 +01:00
|
|
|
package de.mas.jnustool;
|
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.List;
|
|
|
|
|
2016-02-02 19:38:53 +01:00
|
|
|
import de.mas.jnustool.util.Util;
|
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
public class FST {
|
2016-02-02 19:38:53 +01:00
|
|
|
private TitleMetaData tmd;
|
2016-02-01 20:54:01 +01:00
|
|
|
long totalContentSize = 0L;
|
|
|
|
long totalContentSizeInNUS = 0L;
|
|
|
|
|
|
|
|
List<FEntry> fileEntries = new ArrayList<>();
|
|
|
|
|
|
|
|
int totalContentCount = 0;
|
|
|
|
|
|
|
|
int totalEntries = 0;
|
|
|
|
int dirEntries = 0;
|
2016-02-05 16:40:26 +01:00
|
|
|
public FEntry metaFENtry;
|
2016-02-06 18:25:08 +01:00
|
|
|
public List<FEntry> metaFolder = new ArrayList<>();
|
2016-02-02 19:38:53 +01:00
|
|
|
private Directory FSTDirectory = new Directory("root");
|
|
|
|
|
|
|
|
private Directory contentDirectory = new Directory("root");
|
2016-02-01 23:57:01 +01:00
|
|
|
|
2016-02-02 19:38:53 +01:00
|
|
|
public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
|
2016-02-01 20:54:01 +01:00
|
|
|
parse(decrypteddata,tmd);
|
2016-02-02 19:38:53 +01:00
|
|
|
setTmd(tmd);
|
|
|
|
buildDirectories();
|
2016-02-01 23:57:01 +01:00
|
|
|
}
|
|
|
|
|
2016-03-02 19:48:04 +01:00
|
|
|
private void buildDirectories() {
|
2016-02-02 19:38:53 +01:00
|
|
|
String contentfolder = "";
|
|
|
|
Directory curContent = contentDirectory;
|
|
|
|
for(FEntry f : getFileEntries()){
|
2016-02-02 00:03:41 +01:00
|
|
|
if(!f.isDir() && f.isInNUSTitle()){
|
2016-02-02 19:38:53 +01:00
|
|
|
contentfolder = String.format("%08X",tmd.contents[f.getContentID()].ID);
|
|
|
|
|
|
|
|
if(!contentDirectory.containsFolder(contentfolder)){
|
|
|
|
Directory newDir = new Directory(contentfolder);
|
|
|
|
contentDirectory.addFolder(newDir);
|
|
|
|
}
|
|
|
|
curContent = contentDirectory.getFolder(contentfolder);
|
|
|
|
|
|
|
|
Directory current = FSTDirectory;
|
2016-02-02 00:03:41 +01:00
|
|
|
int i = 0;
|
2016-02-02 19:38:53 +01:00
|
|
|
|
|
|
|
for(String s :f.getPathList()){
|
|
|
|
i++;
|
|
|
|
|
|
|
|
//Content
|
|
|
|
if(curContent.containsFolder(s)){
|
|
|
|
curContent = curContent.get(s);
|
|
|
|
}else{
|
|
|
|
Directory newDir = new Directory(s);
|
|
|
|
curContent.addFolder(newDir);
|
|
|
|
curContent = newDir;
|
|
|
|
}
|
|
|
|
if(i==f.getPathList().size()){
|
|
|
|
curContent.addFile(f);
|
2016-03-02 19:48:04 +01:00
|
|
|
}
|
2016-02-02 19:38:53 +01:00
|
|
|
|
|
|
|
//FST
|
2016-02-02 00:03:41 +01:00
|
|
|
if(current.containsFolder(s)){
|
|
|
|
current = current.get(s);
|
|
|
|
}else{
|
|
|
|
Directory newDir = new Directory(s);
|
|
|
|
current.addFolder(newDir);
|
|
|
|
current = newDir;
|
2016-02-02 19:38:53 +01:00
|
|
|
}
|
2016-02-02 00:03:41 +01:00
|
|
|
if(i==f.getPathList().size()){
|
|
|
|
current.addFile(f);
|
2016-02-02 19:38:53 +01:00
|
|
|
}
|
2016-02-02 00:03:41 +01:00
|
|
|
}
|
2016-02-01 23:57:01 +01:00
|
|
|
}
|
|
|
|
}
|
2016-02-01 20:54:01 +01:00
|
|
|
}
|
2016-02-01 23:57:01 +01:00
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
private void parse(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
|
|
|
|
|
|
|
|
if(!Arrays.equals(Arrays.copyOfRange(decrypteddata, 0, 3), new byte[]{0x46,0x53,0x54})){
|
2016-02-05 16:40:26 +01:00
|
|
|
Logger.log(Util.ByteArrayToString(Arrays.copyOfRange(decrypteddata, 0, 3)));
|
2016-02-01 20:54:01 +01:00
|
|
|
System.err.println("Not a FST. Maybe a wrong key?");
|
|
|
|
throw new IllegalArgumentException("File not a FST");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.totalContentCount = Util.getIntFromBytes(decrypteddata, 8);
|
|
|
|
int base_offset = 0x20+totalContentCount*0x20;
|
|
|
|
this.totalEntries = Util.getIntFromBytes(decrypteddata, base_offset+8);
|
|
|
|
int nameOff = base_offset + totalEntries * 0x10;
|
|
|
|
|
|
|
|
int level=0;
|
|
|
|
int[] LEntry = new int[16];
|
|
|
|
int[] Entry = new int[16];
|
|
|
|
|
|
|
|
for(int i = 0;i<this.totalEntries;i++){
|
|
|
|
boolean dir = false;
|
|
|
|
boolean in_nus_title = true;
|
|
|
|
boolean extract_withHash = false;
|
|
|
|
|
|
|
|
long fileOffset;
|
|
|
|
long fileLength;
|
|
|
|
int type;
|
|
|
|
int contentID;
|
|
|
|
|
|
|
|
String filename = "";
|
|
|
|
String path = "";
|
|
|
|
|
|
|
|
if( level > 0)
|
|
|
|
{
|
|
|
|
while( LEntry[level-1] == i )
|
|
|
|
{
|
|
|
|
level--;
|
|
|
|
}
|
2016-03-02 19:48:04 +01:00
|
|
|
}
|
2016-02-01 20:54:01 +01:00
|
|
|
|
|
|
|
int offset = base_offset + i*0x10;
|
|
|
|
|
|
|
|
//getting the type
|
|
|
|
type = (int) decrypteddata[offset]+128;
|
|
|
|
if((type & FEntry.DIR_FLAG) == 1) dir = true;
|
|
|
|
if((type & FEntry.NOT_IN_NUSTITLE_FLAG) == 0 ) in_nus_title = false;
|
|
|
|
|
|
|
|
|
|
|
|
//getting Name
|
|
|
|
decrypteddata[offset] = 0;
|
|
|
|
int nameoff_entry_offset = Util.getIntFromBytes(decrypteddata, offset);
|
|
|
|
int j = 0;
|
|
|
|
int nameoff_entry = nameOff + nameoff_entry_offset;
|
|
|
|
while(decrypteddata[nameoff_entry + j] != 0){j++;}
|
|
|
|
filename = new String(Arrays.copyOfRange(decrypteddata,nameoff_entry, nameoff_entry + j));
|
|
|
|
|
2016-02-05 16:40:26 +01:00
|
|
|
|
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
//getting offsets. save in two ways
|
|
|
|
offset+=4;
|
|
|
|
fileOffset = (long) Util.getIntFromBytes(decrypteddata, offset);
|
|
|
|
offset+=4;
|
|
|
|
fileLength = Util.getIntAsLongFromBytes(decrypteddata, offset);
|
|
|
|
@SuppressWarnings("unused")
|
|
|
|
int parentOffset = (int) fileOffset;
|
|
|
|
int nextOffset = (int) fileLength;
|
|
|
|
|
|
|
|
|
|
|
|
//grabbing flags
|
|
|
|
offset+=4;
|
|
|
|
int flags = Util.getShortFromBytes(decrypteddata, offset);
|
|
|
|
if((flags & FEntry.EXTRACT_WITH_HASH_FLAG) > 0) extract_withHash = true;
|
|
|
|
if((flags & FEntry.CHANGE_OFFSET_FLAG) == 0) fileOffset <<=5;
|
|
|
|
|
|
|
|
//grabbing contentid
|
|
|
|
offset+=2;
|
|
|
|
contentID = Util.getShortFromBytes(decrypteddata, offset) ;
|
|
|
|
|
|
|
|
|
|
|
|
//remember total size
|
|
|
|
this.totalContentSize += fileLength;
|
|
|
|
if(in_nus_title)this.totalContentSizeInNUS += fileLength;
|
|
|
|
|
2016-02-06 18:25:08 +01:00
|
|
|
boolean metafolder = false;
|
|
|
|
|
2016-02-01 23:57:01 +01:00
|
|
|
List<String> pathList = new ArrayList<>();
|
2016-02-01 20:54:01 +01:00
|
|
|
//getting the full path of entry
|
|
|
|
if(dir)
|
|
|
|
{
|
|
|
|
dirEntries++;
|
|
|
|
Entry[level] = i;
|
|
|
|
LEntry[level++] = nextOffset ;
|
|
|
|
if( level > 15 ) // something is wrong!
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
int k = 0;
|
|
|
|
int nameoffoff,nameoff_entrypath;
|
2016-02-06 18:25:08 +01:00
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
for( j=0; j<level; ++j )
|
|
|
|
{
|
|
|
|
nameoffoff = Util.getIntFromBytes(decrypteddata,base_offset+Entry[j]*0x10);
|
|
|
|
k=0;
|
|
|
|
nameoff_entrypath = nameOff + nameoffoff;
|
|
|
|
while(decrypteddata[nameoff_entrypath + k] != 0){k++;}
|
2016-02-01 23:57:01 +01:00
|
|
|
String tmpname = new String(Arrays.copyOfRange(decrypteddata,nameoff_entrypath, nameoff_entrypath + k));
|
2016-02-06 18:25:08 +01:00
|
|
|
if(j==1 && tmpname.equals("meta")){
|
|
|
|
metafolder = true;
|
|
|
|
}
|
2016-02-01 23:57:01 +01:00
|
|
|
if(!tmpname.equals("")){
|
2016-02-06 18:25:08 +01:00
|
|
|
pathList.add(tmpname);
|
2016-02-05 16:40:26 +01:00
|
|
|
}
|
2016-02-01 23:57:01 +01:00
|
|
|
|
|
|
|
sb.append(tmpname);
|
2016-02-01 20:54:01 +01:00
|
|
|
sb.append("/");
|
|
|
|
}
|
|
|
|
path = sb.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
//add this to the List!
|
2016-02-05 16:40:26 +01:00
|
|
|
FEntry tmp = new FEntry(path,filename,contentID,tmd.contents[contentID].ID,fileOffset,fileLength,dir,in_nus_title,extract_withHash,pathList,this);
|
|
|
|
fileEntries.add(tmp);
|
|
|
|
if(filename.equals("meta.xml")){
|
|
|
|
metaFENtry = tmp;
|
|
|
|
}
|
2016-02-06 18:25:08 +01:00
|
|
|
if(metafolder){
|
|
|
|
metaFolder.add(tmp);
|
|
|
|
}
|
2016-02-05 16:40:26 +01:00
|
|
|
//Logger.log(fileEntries.get(i));
|
2016-02-01 20:54:01 +01:00
|
|
|
}
|
|
|
|
|
2016-03-02 19:48:04 +01:00
|
|
|
}
|
2016-02-01 20:54:01 +01:00
|
|
|
|
|
|
|
public long getTotalContentSize() {
|
|
|
|
return totalContentSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setTotalContentSize(long totalContentSize) {
|
|
|
|
this.totalContentSize = totalContentSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
public long getTotalContentSizeInNUS() {
|
|
|
|
return totalContentSizeInNUS;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setTotalContentSizeInNUS(long totalContentSizeInNUS) {
|
|
|
|
this.totalContentSizeInNUS = totalContentSizeInNUS;
|
|
|
|
}
|
|
|
|
|
2016-02-06 18:25:08 +01:00
|
|
|
public List<FEntry> getMetaFolder() {
|
|
|
|
return metaFolder;
|
2016-03-02 19:48:04 +01:00
|
|
|
}
|
2016-02-01 20:54:01 +01:00
|
|
|
|
|
|
|
public List<FEntry> getFileEntries() {
|
|
|
|
return fileEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setFileEntries(List<FEntry> fileEntries) {
|
|
|
|
this.fileEntries = fileEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getTotalContentCount() {
|
|
|
|
return totalContentCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setTotalContentCount(int totalContentCount) {
|
|
|
|
this.totalContentCount = totalContentCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getTotalEntries() {
|
|
|
|
return totalEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setTotalEntries(int totalEntries) {
|
|
|
|
this.totalEntries = totalEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getDirEntries() {
|
|
|
|
return dirEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void setDirEntries(int dirEntries) {
|
|
|
|
this.dirEntries = dirEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String toString(){
|
|
|
|
return "entryCount: " + totalContentCount+ " entries: " + totalEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getFileCount() {
|
|
|
|
int i = 0;
|
|
|
|
for(FEntry f: getFileEntries()){
|
|
|
|
if(!f.isDir())
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getFileCountInNUS() {
|
|
|
|
int i = 0;
|
|
|
|
for(FEntry f: getFileEntries()){
|
|
|
|
if(!f.isDir() && f.isInNUSTitle())
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return i;
|
|
|
|
}
|
2016-03-02 21:00:42 +01:00
|
|
|
|
|
|
|
public String notInNUS() {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
for(FEntry f: getFileEntries()){
|
|
|
|
if(!f.isDir() && !f.isInNUSTitle()){
|
|
|
|
sb.append(f.getFullPath() + " " + String.format("%8.2f MB ", f.getFileLength()/1024.0/1024.0) + "\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sb.toString();
|
|
|
|
}
|
2016-02-01 23:57:01 +01:00
|
|
|
|
2016-02-02 19:38:53 +01:00
|
|
|
public Directory getFSTDirectory() {
|
|
|
|
return FSTDirectory;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Directory getContentDirectory() {
|
|
|
|
return contentDirectory;
|
|
|
|
}
|
|
|
|
|
|
|
|
public TitleMetaData getTmd() {
|
|
|
|
return tmd;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setTmd(TitleMetaData tmd) {
|
|
|
|
this.tmd = tmd;
|
2016-02-01 23:57:01 +01:00
|
|
|
}
|
2016-02-02 19:38:53 +01:00
|
|
|
|
2016-02-01 23:57:01 +01:00
|
|
|
|
|
|
|
|
2016-02-01 20:54:01 +01:00
|
|
|
}
|