mirror of
https://github.com/Maschell/JNUSTool.git
synced 2024-11-27 18:34:18 +01:00
Several bugfixes, updated the CSV, and added the filelist.txt creation when downloading updates
Also started to add some comments (finally) :D
This commit is contained in:
parent
92c6de805e
commit
d9dd8371b4
BIN
JNUSTool.jar
Normal file
BIN
JNUSTool.jar
Normal file
Binary file not shown.
BIN
jar/JNUSTool.jar
BIN
jar/JNUSTool.jar
Binary file not shown.
@ -247,16 +247,16 @@
|
||||
0005000E-10189200;2;WUP;011C;WUP-P-BSPE;BSPE1C;Sportsball;16
|
||||
0005000E-1018C400;1;WUP;00AF;WUP-P-BSFJ;BSFJAF;???F??????????? ????SF???????????;16
|
||||
0005000E-1018D900;1;WUP;00AF;WUP-P-APHJ;APHJAF;LOST REAVERS;17
|
||||
0005000E-1018DB00;1;WUP;0001;WUP-U-AMAJ;AMAJ01;Super Mario Maker;16,32,48,64,80,96,113,128,144,160
|
||||
0005000E-1018DC00;2;WUP;0001;WUP-U-AMAE;AMAE01;Super Mario Maker;16,32,48,64,80,96,113,128,144,160
|
||||
0005000E-1018DD00;4;WUP;0001;WUP-U-AMAP;AMAP01;Super Mario Maker;32,48,64,80,96,113,128,144,160
|
||||
0005000E-1018DB00;1;WUP;0001;WUP-U-AMAJ;AMAJ01;Super Mario Maker;16,32,48,64,80,96,113,128,144,160,176
|
||||
0005000E-1018DC00;2;WUP;0001;WUP-U-AMAE;AMAE01;Super Mario Maker;16,32,48,64,80,96,113,128,144,160,176
|
||||
0005000E-1018DD00;4;WUP;0001;WUP-U-AMAP;AMAP01;Super Mario Maker;32,48,64,80,96,113,128,144,160,176
|
||||
0005000E-1018DE00;4;WUP;00NK;WUP-U-ATCP;ATCPNK;Tetrobot and Co.;16
|
||||
0005000E-1018ED00;2;WUP;00NK;WUP-U-ATCE;ATCENK;Tetrobot and Co.;16
|
||||
0005000E-10191700;4;WUP;00UP;WUP-U-BMVP;BMVPUP;99Moves;16,32
|
||||
0005000E-10191800;4;WUP;00UP;WUP-U-WAEP;WAEPUP;Rock 'N Racing Off Road;16
|
||||
0005000E-10192200;1;WUP;00AF;WUP-P-BRSJ;BRSJAF;KAMEN RIDER SUMMON RIDE!;16,33,48,64,80
|
||||
0005000E-10193300;4;WUP;006V;WUP-P-BLKP;BLKP6V;Legend of Kay Anniversary;16
|
||||
0005000E-10194200;2;WUP;00WR;WUP-P-APZE;APZEWR;LEGO® DIMENSIONS™;16,32,48,64,80,96
|
||||
0005000E-10194200;2;WUP;00WR;WUP-P-APZE;APZEWR;LEGO® DIMENSIONS™;16,32,48,64,80,96,112
|
||||
0005000E-10195D00;4;WUP;00WR;WUP-P-APZP;APZPWR;LEGO® DIMENSIONS™;32,48,64,80,96,112,128
|
||||
0005000E-10197800;4;WUP;011S;WUP-P-ACQP;ACQP1S;Costume Quest 2;16
|
||||
0005000E-10199000;1;WUP;0001;WUP-P-AVXJ;AVXJ01;Mario Tennis: Ultra Smash;16,32
|
||||
@ -324,14 +324,15 @@
|
||||
0005000E-101D6100;4;WUP;00FV;WUP-U-AGWP;AGWPFV;Gunman Clive HD Collection;16
|
||||
0005000E-101D6D00;4;WUP;010P;WUP-P-ARNP;ARNP0P;Runbow;17,33
|
||||
0005000E-101D7400;4;WUP;010C;WUP-U-ATTP;ATTP0C;Totem Topple;16
|
||||
0005000E-101D7500;4;WUP;00DU;WUP-P-AUMP;AUMPDU;Minecraft: Wii U Edition;16,32,48
|
||||
0005000E-101D7500;4;WUP;00DU;WUP-P-AUMP;AUMPDU;Minecraft: Wii U Edition;16,32,48,64
|
||||
0005000E-101D8300;4;WUP;00TJ;WUP-U-AEJP;AEJPTJ;Electronic Super Joy;16
|
||||
0005000E-101D9600;4;WUP;00UP;WUP-U-ARXP;ARXPUP;Rock 'N Racing Off Road DX;16
|
||||
0005000E-101D9D00;2;WUP;00DU;WUP-P-AUME;AUMEDU;Minecraft: Wii U Edition;16,32,48
|
||||
0005000E-101DBE00;1;WUP;0188;WUP-P-AUMJ;AUMJ88;Minecraft: Wii U Edition;16,32,48
|
||||
0005000E-101D9D00;2;WUP;00DU;WUP-P-AUME;AUMEDU;Minecraft: Wii U Edition;16,32,48,64
|
||||
0005000E-101DBE00;1;WUP;0188;WUP-P-AUMJ;AUMJ88;Minecraft: Wii U Edition;16,32,48,64
|
||||
0005000E-101DC100;4;WUP;016E;WUP-P-AD5P;AD5P6E;Octodad: Dadliest Catch;16
|
||||
0005000E-101DCA00;4;WUP;017D;WUP-P-AVMP;AVMP7D;VoxelMaker;16
|
||||
0005000E-101DD700;1;WUP;013P;WUP-P-ARNJ;ARNJ3P;Runbow;16,17
|
||||
0005000E-101DDC00;1;WUP;00AF;WUP-P-BD3J;BD3JAF;????????????3.0;17,33,48
|
||||
0005000E-101DF400;2;WUP;0001;WUP-P-APKE;APKE01;POKKÉN TOURNAMENT;16
|
||||
0005000E-101DF400;2;WUP;0001;WUP-P-APKE;APKE01;POKKÉN TOURNAMENT;16,32
|
||||
0005000E-101DF500;4;WUP;0001;WUP-P-APKP;APKP01;POKKÉN TOURNAMENT;16
|
||||
0005000E-101E4300;1;WUP;00TF;WUP-P-AMPJ;AMPJTF;???? ????? ???????;16
|
||||
|
|
BIN
release.zip
BIN
release.zip
Binary file not shown.
@ -7,17 +7,24 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import de.mas.jnustool.util.Downloader;
|
||||
import de.mas.jnustool.util.Settings;
|
||||
import de.mas.jnustool.util.Util;
|
||||
|
||||
/**
|
||||
* Content file of the NUSTitle. Holds the encrpyted files
|
||||
*
|
||||
* Thanks to crediar for the offsets in CDecrypt
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class Content {
|
||||
|
||||
/**
|
||||
* TODO: make it simpler
|
||||
*/
|
||||
int ID; // 0 0xB04
|
||||
short index; // 4 0xB08
|
||||
short type; // 6 0xB0A
|
||||
long size; // 8 0xB0C
|
||||
byte[] SHA2 = new byte[32]; // 16 0xB14
|
||||
TitleMetaData tmd;
|
||||
AtomicInteger error_output_done = new AtomicInteger(0);// 16 0xB14
|
||||
|
||||
AtomicInteger error_output_done = new AtomicInteger(0);
|
||||
|
||||
public Content(int ID, short index, short type, long size, byte[] SHA2,TitleMetaData tmd) {
|
||||
this.ID = ID;
|
||||
@ -27,23 +34,23 @@ public class Content {
|
||||
this.SHA2 = SHA2;
|
||||
this.tmd = tmd;
|
||||
}
|
||||
@Override
|
||||
public String toString(){
|
||||
return "ID: " + ID +" index: " + index + " type: " + type + " size: " + size + " SHA2: " + Util.ByteArrayToString(SHA2);
|
||||
}
|
||||
|
||||
public void download(Progress progress) throws IOException{
|
||||
/**
|
||||
* Downloads the content files (encrypted)
|
||||
* @param progress: A progress object can be used to get informations of the progress. Will be ignored when null is used.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void download(Progress progress) throws IOException{
|
||||
String tmpPath = tmd.getContentPath();
|
||||
if ((type & 0x02) == 0x02){
|
||||
Downloader.getInstance().downloadContentH3(tmd.titleID,ID,tmpPath,null);
|
||||
}
|
||||
|
||||
File f = new File(tmpPath + "/" + String.format("%08X", ID ) + ".app");
|
||||
if(f.exists()){
|
||||
if(f.length() == size){
|
||||
Logger.log("Skipping Content: " + String.format("%08X", ID));
|
||||
progress.addCurrent((int) size);
|
||||
return;
|
||||
}else{
|
||||
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
||||
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
||||
Logger.log("Content " +String.format("%08X", ID) + " is broken. Downloading it again.");
|
||||
Downloader.getInstance().downloadContent(tmd.titleID,ID,tmpPath,progress);
|
||||
}else{
|
||||
@ -59,6 +66,13 @@ public class Content {
|
||||
Logger.log("Download Content: " + String.format("%08X", ID));
|
||||
Downloader.getInstance().downloadContent(tmd.titleID,ID,tmpPath,progress);
|
||||
}
|
||||
|
||||
if ((type & 0x02) == 0x02){
|
||||
Downloader.getInstance().downloadContentH3(tmd.titleID,ID,tmpPath,null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "ID: " + ID +" index: " + index + " type: " + type + " size: " + size + " SHA2: " + Util.ByteArrayToString(SHA2);
|
||||
}
|
||||
}
|
||||
|
@ -2,30 +2,50 @@ package de.mas.jnustool;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Callable Class to download Contents
|
||||
* Has no return value
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class ContentDownloader implements Callable<Integer>
|
||||
{
|
||||
Content c;
|
||||
Content content;
|
||||
Progress progress = null;
|
||||
public void setContent(Content c){
|
||||
this.c = c;
|
||||
}
|
||||
public ContentDownloader(Content f,Progress fatherProgress){
|
||||
setContent(f);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param content: the content that need to be downloaded
|
||||
* @param fatherProgress: father progress that need be informed about the progress. (Can be NULL)
|
||||
*/
|
||||
public ContentDownloader(Content content,Progress fatherProgress){
|
||||
setContent(content);
|
||||
createProgress(fatherProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content that need to be downloaded
|
||||
* @param content
|
||||
*/
|
||||
public void setContent(Content content){
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sub process for this content.
|
||||
* @param fatherProgress: the father progress
|
||||
*/
|
||||
private void createProgress(Progress fatherProgress) {
|
||||
if(fatherProgress != null){
|
||||
progress = new Progress();
|
||||
fatherProgress.add(progress);
|
||||
progress.addTotal(c.size);
|
||||
|
||||
progress.addTotal(this.content.size);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
c.download(progress);
|
||||
this.content.download(progress);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,21 @@
|
||||
package de.mas.jnustool;
|
||||
|
||||
import de.mas.jnustool.util.Util;
|
||||
|
||||
/**
|
||||
* Stores informations of the content [...]
|
||||
*
|
||||
*
|
||||
* Thanks to crediar for the offsets in CDecrypt
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class ContentInfo {
|
||||
public short indexOffset; // 0 0x204
|
||||
public short indexOffset; // 0 0x204
|
||||
public short commandCount; // 2 0x206
|
||||
public byte[] SHA2 = new byte[32]; // 12 0x208
|
||||
|
||||
//TODO: Test, size checking
|
||||
/*
|
||||
* untested
|
||||
*/
|
||||
|
||||
//TODO: Test, size checking. Untested right now
|
||||
|
||||
public ContentInfo(byte[] info){
|
||||
this.indexOffset=(short)( ((info[0]&0xFF)<<8) | (info[1]&0xFF) );
|
||||
this.commandCount=(short)( ((info[2]&0xFF)<<8) | (info[3]&0xFF) );
|
||||
@ -25,6 +29,7 @@ public class ContentInfo {
|
||||
this.commandCount = commandCount;
|
||||
this.SHA2 = SHA2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "indexOffset: " + indexOffset +" commandCount: " + commandCount + " SHA2: " + Util.ByteArrayToString(SHA2);
|
||||
|
@ -4,41 +4,68 @@ import java.util.Collection;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
public class Directory {
|
||||
/**
|
||||
* A Class that represents a Directory of a NUSTitle file table.
|
||||
* Every directory can hold other directories and Objects that implements the IHasName Interface. Build in a tree structure.
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class Directory<T extends IHasName> {
|
||||
String name = "";
|
||||
TreeMap<String,Directory> folder = new TreeMap<>();
|
||||
TreeMap<String,FEntry> files = new TreeMap<>();
|
||||
|
||||
public Directory get(String s){
|
||||
return folder.get(s);
|
||||
}
|
||||
TreeMap<String,Directory<T>> folder = new TreeMap<>();
|
||||
TreeMap<String,T> files = new TreeMap<>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name Name of this Directory
|
||||
*/
|
||||
public Directory(String name){
|
||||
setName(name);
|
||||
}
|
||||
|
||||
public boolean containsFolder(String s){
|
||||
return folder.containsKey(s);
|
||||
|
||||
/**
|
||||
* Checks if a sub directory with the given name exits
|
||||
* @param name Name of the sub directory
|
||||
* @return true is the sub directory exists
|
||||
*/
|
||||
public boolean containsFolder(String name){
|
||||
return folder.containsKey(name);
|
||||
}
|
||||
/**
|
||||
* Returns a sub directory with the given name
|
||||
* or {@code null} if this directory contains no mapping for the name.
|
||||
* @param name Name of the sub directory
|
||||
* @return The sub directory or null
|
||||
*/
|
||||
public Directory<T> getFolder(String name){
|
||||
return folder.get(name);
|
||||
}
|
||||
|
||||
public Directory getFolder(String s){
|
||||
return folder.get(s);
|
||||
/**
|
||||
* Adds a sub directory
|
||||
* @param subDirectory the sub directory.
|
||||
* @return the previous directory associated with the same name, or
|
||||
* {@code null} if there was no directoy with this name.
|
||||
*/
|
||||
public Directory<T> addFolder(Directory<T> subDirectory){
|
||||
return folder.put(subDirectory.getName(),subDirectory);
|
||||
}
|
||||
|
||||
public Directory addFolder(Directory s){
|
||||
return folder.put(s.getName(),s);
|
||||
}
|
||||
public boolean containsFile(String s){
|
||||
return files.containsKey(s);
|
||||
/**
|
||||
* Checks if a object with the given name exits. This will NOT check the sub directories
|
||||
* @param name name of the object
|
||||
* @return true is a object with the same name exists.
|
||||
*/
|
||||
public boolean containsFile(String name){
|
||||
return files.containsKey(name);
|
||||
}
|
||||
|
||||
public FEntry getFile(String s){
|
||||
public T getFile(String s){
|
||||
return files.get(s);
|
||||
}
|
||||
|
||||
public FEntry addFile(FEntry s){
|
||||
return files.put(s.getFileName(),s);
|
||||
public T addFile(T s){
|
||||
return files.put(s.getName(),s);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@ -47,28 +74,38 @@ public class Directory {
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Collection<Directory> getFolder() {
|
||||
public Collection<Directory<T>> getFolder() {
|
||||
return folder.values();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Collection<FEntry> getFiles() {
|
||||
public Collection<T> getFiles() {
|
||||
return files.values();
|
||||
}
|
||||
|
||||
public void setFiles(TreeMap<String, FEntry> files) {
|
||||
public void setFiles(TreeMap<String, T> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
|
||||
public DefaultMutableTreeNode getNodes(){
|
||||
DefaultMutableTreeNode node = new DefaultMutableTreeNode(getName());
|
||||
|
||||
for(Directory<T> f: getFolder()){
|
||||
node.add(f.getNodes());
|
||||
}
|
||||
|
||||
for(T f: getFiles()){
|
||||
node.add(new DefaultMutableTreeNode(f));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(name + ":" + "\n");
|
||||
for(Directory d : folder.values()){
|
||||
for(Directory<T> d : folder.values()){
|
||||
sb.append(d + "\n");
|
||||
}
|
||||
for(String s : files.keySet()){
|
||||
@ -77,18 +114,5 @@ public class Directory {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public DefaultMutableTreeNode getNodes(){
|
||||
DefaultMutableTreeNode node = new DefaultMutableTreeNode(getName());
|
||||
|
||||
for(Directory f: getFolder()){
|
||||
node.add(f.getNodes());
|
||||
}
|
||||
|
||||
for(FEntry f: getFiles()){
|
||||
node.add(new DefaultMutableTreeNode(f));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import de.mas.jnustool.util.Downloader;
|
||||
import de.mas.jnustool.util.Settings;
|
||||
import de.mas.jnustool.util.Util;
|
||||
|
||||
public class FEntry {
|
||||
public class FEntry implements IHasName{
|
||||
private FST fst;
|
||||
|
||||
public static int DIR_FLAG = 1;
|
||||
@ -233,6 +233,11 @@ public class FEntry {
|
||||
return Downloader.getInstance().downloadAndDecrypt(this,null,true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getFileName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -20,30 +20,31 @@ public class FST {
|
||||
int dirEntries = 0;
|
||||
public FEntry metaFENtry;
|
||||
public List<FEntry> metaFolder = new ArrayList<>();
|
||||
private Directory FSTDirectory = new Directory("root");
|
||||
private Directory<FEntry> FSTDirectory = new Directory<FEntry>("root");
|
||||
|
||||
private Directory contentDirectory = new Directory("root");
|
||||
private Directory<FEntry> contentDirectory = new Directory<FEntry>("root");
|
||||
|
||||
public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
|
||||
parse(decrypteddata,tmd);
|
||||
setTmd(tmd);
|
||||
buildDirectories();
|
||||
|
||||
}
|
||||
|
||||
private void buildDirectories() {
|
||||
String contentfolder = "";
|
||||
Directory curContent = contentDirectory;
|
||||
Directory<FEntry> curContent = contentDirectory;
|
||||
for(FEntry f : getFileEntries()){
|
||||
if(!f.isDir() && f.isInNUSTitle()){
|
||||
contentfolder = String.format("%08X",tmd.contents[f.getContentID()].ID);
|
||||
|
||||
if(!contentDirectory.containsFolder(contentfolder)){
|
||||
Directory newDir = new Directory(contentfolder);
|
||||
Directory<FEntry> newDir = new Directory<FEntry>(contentfolder);
|
||||
contentDirectory.addFolder(newDir);
|
||||
}
|
||||
curContent = contentDirectory.getFolder(contentfolder);
|
||||
|
||||
Directory current = FSTDirectory;
|
||||
Directory<FEntry> current = FSTDirectory;
|
||||
int i = 0;
|
||||
|
||||
for(String s :f.getPathList()){
|
||||
@ -51,9 +52,9 @@ public class FST {
|
||||
|
||||
//Content
|
||||
if(curContent.containsFolder(s)){
|
||||
curContent = curContent.get(s);
|
||||
curContent = curContent.getFolder(s);
|
||||
}else{
|
||||
Directory newDir = new Directory(s);
|
||||
Directory<FEntry> newDir = new Directory<FEntry>(s);
|
||||
curContent.addFolder(newDir);
|
||||
curContent = newDir;
|
||||
}
|
||||
@ -63,9 +64,9 @@ public class FST {
|
||||
|
||||
//FST
|
||||
if(current.containsFolder(s)){
|
||||
current = current.get(s);
|
||||
current = current.getFolder(s);
|
||||
}else{
|
||||
Directory newDir = new Directory(s);
|
||||
Directory<FEntry> newDir = new Directory<FEntry>(s);
|
||||
current.addFolder(newDir);
|
||||
current = newDir;
|
||||
}
|
||||
@ -81,10 +82,11 @@ public class FST {
|
||||
|
||||
if(!Arrays.equals(Arrays.copyOfRange(decrypteddata, 0, 3), new byte[]{0x46,0x53,0x54})){
|
||||
Logger.log(Util.ByteArrayToString(Arrays.copyOfRange(decrypteddata, 0, 3)));
|
||||
|
||||
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);
|
||||
@ -296,11 +298,11 @@ public class FST {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Directory getFSTDirectory() {
|
||||
public Directory<FEntry> getFSTDirectory() {
|
||||
return FSTDirectory;
|
||||
}
|
||||
|
||||
public Directory getContentDirectory() {
|
||||
public Directory<FEntry> getContentDirectory() {
|
||||
return contentDirectory;
|
||||
}
|
||||
|
||||
|
5
src/de/mas/jnustool/IHasName.java
Normal file
5
src/de/mas/jnustool/IHasName.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.mas.jnustool;
|
||||
|
||||
public interface IHasName {
|
||||
public String getName();
|
||||
}
|
@ -10,8 +10,7 @@ public class Logger {
|
||||
public static void log(String string) {
|
||||
NUSGUI.output.append(string + "\n");
|
||||
NUSGUI.output.setCaretPosition(NUSGUI.output.getDocument().getLength());
|
||||
System.out.println(string);
|
||||
|
||||
//System.out.println(string);
|
||||
}
|
||||
|
||||
public static void messageBox(String string) {
|
||||
|
@ -25,7 +25,7 @@ import de.mas.jnustool.util.NUSTitleInformation;
|
||||
import de.mas.jnustool.util.Settings;
|
||||
import de.mas.jnustool.util.Util;
|
||||
|
||||
public class NUSTitle {
|
||||
public class NUSTitle {
|
||||
private TitleMetaData tmd;
|
||||
private TIK ticket;
|
||||
private FST fst;
|
||||
@ -34,9 +34,12 @@ public class NUSTitle {
|
||||
private String longNameFolder = new String();
|
||||
private int version = -1;
|
||||
|
||||
|
||||
private String getTMDName(){
|
||||
String result = "title.tmd";
|
||||
|
||||
if(version > 0 && Settings.DL_ALL_VERSIONS){
|
||||
result += "." + version;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -155,35 +158,34 @@ public class NUSTitle {
|
||||
Logger.log("Downloading of missing files is not enabled. Exiting");
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
decryption.init(ticket.getDecryptedKey(), 0);
|
||||
decryption.init(ticket.getDecryptedKey(),0);
|
||||
byte[] decryptedFST = decryption.decrypt(encryptedFST);
|
||||
|
||||
fst = new FST(decryptedFST,tmd);
|
||||
|
||||
tmd.setNUSTitle(this);
|
||||
|
||||
setTargetPath(String.format("%016X", getTitleID()));
|
||||
setTargetPath(String.format("%016X", getTitleID()));
|
||||
setLongNameFolder(String.format("%016X", getTitleID()));
|
||||
|
||||
setLongNameFolder(String.format("%016X", getTitleID()));
|
||||
|
||||
|
||||
byte[] metaxml = fst.metaFENtry.downloadAsByteArray();
|
||||
if(metaxml != null){
|
||||
InputStream bis = new ByteArrayInputStream(metaxml);
|
||||
NUSTitleInformation nusinfo = readMeta(bis);
|
||||
String folder = nusinfo.getLongnameEN() + " [" + nusinfo.getID6() + "]";
|
||||
String subfolder = "";
|
||||
if(tmd.isUpdate()) subfolder = "/" + "updates" + "/" + "v" + tmd.titleVersion;
|
||||
setTargetPath(folder + subfolder);
|
||||
setLongNameFolder(folder);
|
||||
if(fst.metaFENtry != null){
|
||||
byte[] metaxml = fst.metaFENtry.downloadAsByteArray();
|
||||
if(metaxml != null){
|
||||
InputStream bis = new ByteArrayInputStream(metaxml);
|
||||
NUSTitleInformation nusinfo = readMeta(bis);
|
||||
String folder = nusinfo.getLongnameEN() + " [" + nusinfo.getID6() + "]";
|
||||
String subfolder = "";
|
||||
if(tmd.isUpdate()) subfolder = "/" + "updates" + "/" + "v" + tmd.titleVersion;
|
||||
setTargetPath(folder + subfolder);
|
||||
setLongNameFolder(folder);
|
||||
}
|
||||
}
|
||||
|
||||
if(Settings.downloadContent){
|
||||
downloadEncryptedFiles(null);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log("Total Size of Content Files: " + ((int)((getTotalContentSize()/1024.0/1024.0)*100))/100.0 +" MB");
|
||||
Logger.log("Total Size of Decrypted Files: " + ((int)((fst.getTotalContentSizeInNUS()/1024.0/1024.0)*100))/100.0 +" MB");
|
||||
@ -198,6 +200,7 @@ public class NUSTitle {
|
||||
}
|
||||
|
||||
public void downloadEncryptedFiles(Progress progress) throws IOException {
|
||||
|
||||
Util.createSubfolder(getContentPath());
|
||||
|
||||
Downloader.getInstance().downloadTMD(titleID,version,getContentPath());
|
||||
@ -209,12 +212,16 @@ public class NUSTitle {
|
||||
fos.write(tmd.cert);
|
||||
fos.write(ticket.cert1);
|
||||
fos.close();
|
||||
if(version > 0 && Settings.DL_ALL_VERSIONS){
|
||||
fos = new FileOutputStream(getContentPath() + "/title.cert." + version);
|
||||
fos.write(ticket.cert0);
|
||||
fos.write(tmd.cert);
|
||||
fos.write(ticket.cert1);
|
||||
fos.close();
|
||||
}
|
||||
}catch(Exception e){
|
||||
Logger.log("Random error.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
NUSTitleInformation readMeta(InputStream bis) {
|
||||
@ -278,7 +285,7 @@ public class NUSTitle {
|
||||
|
||||
public String getContentPath() {
|
||||
String result = getContentPathPrefix() + String.format("%016X", getTitleID());
|
||||
if(version > 0){
|
||||
if(version > 0 && !Settings.DL_ALL_VERSIONS){ //Only add the prefix when we don't download all version of that title
|
||||
result += "_v" + version;
|
||||
}
|
||||
return result;
|
||||
@ -307,11 +314,12 @@ public class NUSTitle {
|
||||
}
|
||||
}
|
||||
pool.invokeAll(dlList);
|
||||
if(tmd.isUpdate()) Util.buildFileList(getTargetPath(),"content",Settings.FILELIST_NAME);
|
||||
Logger.log("Done!");
|
||||
}
|
||||
|
||||
|
||||
public void setTargetPath(String path){
|
||||
path = path.replaceAll("[:\\\\*?|<>]", "");
|
||||
path = Util.replaceCharsInString(path);
|
||||
this.targetPath = path;
|
||||
}
|
||||
|
||||
@ -324,7 +332,7 @@ public class NUSTitle {
|
||||
}
|
||||
|
||||
public void setLongNameFolder(String path) {
|
||||
path = path.replaceAll("[:\\\\*?|<>]", "");
|
||||
path = Util.replaceCharsInString(path);
|
||||
longNameFolder = path;
|
||||
}
|
||||
|
||||
@ -335,7 +343,4 @@ public class NUSTitle {
|
||||
public void setVersion(int version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ public class Progress {
|
||||
}
|
||||
|
||||
private void setCurrent(long current) {
|
||||
this.current.set(current);
|
||||
update();
|
||||
this.current.set(current);
|
||||
update();
|
||||
}
|
||||
|
||||
public void addTotal(long i) {
|
||||
@ -62,7 +62,7 @@ public class Progress {
|
||||
private void update() {
|
||||
if(father != null) father.update();
|
||||
|
||||
if(progressUpdateListener != null){
|
||||
if(progressUpdateListener != null){
|
||||
progressUpdateListener.updatePerformed(this);
|
||||
}
|
||||
}
|
||||
@ -114,7 +114,7 @@ public class Progress {
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
setCurrent(getTotalOfSingle());
|
||||
addCurrent((int) getTotalOfSingle());
|
||||
}
|
||||
|
||||
private boolean inprogress = false;
|
||||
|
@ -10,6 +10,7 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import de.mas.jnustool.gui.NUSGUI;
|
||||
import de.mas.jnustool.gui.UpdateChooser;
|
||||
@ -23,7 +24,7 @@ public class Starter {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
Logger.log("JNUSTool 0.0.5 - alpha - by Maschell");
|
||||
Logger.log("JNUSTool 0.0.6 - alpha - by Maschell");
|
||||
Logger.log("");
|
||||
try {
|
||||
readConfig();
|
||||
@ -31,6 +32,7 @@ public class Starter {
|
||||
System.err.println("Error while reading config! Needs to be:");
|
||||
System.err.println("DOWNLOAD URL BASE");
|
||||
System.err.println("COMMONKEY");
|
||||
System.err.println("updateinfos.csv");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -108,7 +110,7 @@ public class Starter {
|
||||
String product_code = infos[4];
|
||||
String ID6 = infos[5];
|
||||
String longnameEN = infos[6];
|
||||
String[] versions = infos[7].split(",");
|
||||
String[] versions = infos[7].split(",");
|
||||
NUSTitleInformation info = new NUSTitleInformation(titleID, longnameEN, ID6, product_code, content_platform, company_code, region,versions);
|
||||
|
||||
list.add(info);
|
||||
@ -131,7 +133,7 @@ public class Starter {
|
||||
public static void readConfig() throws IOException {
|
||||
BufferedReader in = new BufferedReader(new FileReader(new File("config")));
|
||||
Downloader.URL_BASE = in.readLine();
|
||||
String commonkey = in.readLine();
|
||||
String commonkey = in.readLine();
|
||||
if(commonkey.length() != 32){
|
||||
Logger.messageBox("CommonKey length is wrong");
|
||||
Logger.log("Commonkey length is wrong");
|
||||
@ -223,4 +225,45 @@ public class Starter {
|
||||
|
||||
}
|
||||
|
||||
public static AtomicInteger finished = new AtomicInteger();
|
||||
|
||||
public static void downloadEncryptedAllVersions(List<NUSTitleInformation> output_, Progress progress) {
|
||||
ForkJoinPool pool = new ForkJoinPool(25);
|
||||
|
||||
List<ForkJoinTask<Boolean>> list = new ArrayList<>();
|
||||
|
||||
for(NUSTitleInformation nus : output_){
|
||||
final long tID = nus.getTitleID();
|
||||
list.add(pool.submit(new Callable<Boolean>(){
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
int count = 1;
|
||||
for(Integer i : nus.getAllVersions()){
|
||||
NUSTitle nusa = new NUSTitle(tID,i, Util.ByteArrayToString(nus.getKey()));
|
||||
Progress childProgress = new Progress();
|
||||
progress.add(childProgress);
|
||||
nusa.downloadEncryptedFiles(progress);
|
||||
System.out.println("Update download progress " + "(" + nus.getLongnameEN() + ") version "+ i + " complete! This was " + count + " of " + nus.getAllVersions().size() + "!");
|
||||
count++;
|
||||
}
|
||||
System.out.println("Update download complete " + "(" + nus.getLongnameEN() +")" +"! Loaded updates for " + nus.getAllVersions().size() + " version. Now are " + finished.incrementAndGet() + " of " + output_.size() + " done! ");
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for(ForkJoinTask<Boolean> task : list){
|
||||
try {
|
||||
task.get();
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (ExecutionException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,8 +9,16 @@ import java.util.List;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
import de.mas.jnustool.util.Util;
|
||||
|
||||
/**
|
||||
* The TitleMetaData (TMD) stores information of the NUSTitle. [...]
|
||||
*
|
||||
*
|
||||
* Thanks to crediar for the offsets in CDecrypt
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class TitleMetaData {
|
||||
//TODO: cleanup
|
||||
int signatureType; // 0x000
|
||||
byte[] signature = new byte[0x100]; // 0x004
|
||||
byte[] issuer = new byte[0x40]; // 0x140
|
||||
@ -43,18 +51,24 @@ public class TitleMetaData {
|
||||
public TitleMetaData(byte[] downloadTMDToByteArray) throws IOException {
|
||||
if(downloadTMDToByteArray != null){
|
||||
File tempFile;
|
||||
tempFile = File.createTempFile("bla","blubb");
|
||||
tempFile = File.createTempFile("jnustool",".tmp");
|
||||
FileOutputStream fos = new FileOutputStream(tempFile);
|
||||
fos.write(downloadTMDToByteArray);
|
||||
fos.close();
|
||||
parse(tempFile);
|
||||
setTotalContentSize();
|
||||
|
||||
}else{
|
||||
System.err.println("Invalid TMD");
|
||||
throw new IllegalArgumentException("Invalid TMD");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a TMD file
|
||||
* @param tmd
|
||||
* @throws IOException
|
||||
*/
|
||||
private void parse(File tmd) throws IOException {
|
||||
|
||||
RandomAccessFile f = new RandomAccessFile(tmd, "r");
|
||||
@ -143,6 +157,7 @@ public class TitleMetaData {
|
||||
sb.append("SHA2: " + Util.ByteArrayToString(SHA2) +"\n");
|
||||
sb.append("cert: " + Util.ByteArrayToString(cert) +"\n");
|
||||
sb.append("contentInfos: \n");
|
||||
|
||||
for(int i = 0; i<contents.length-1;i++){
|
||||
sb.append(" " + contentInfos[i] +"\n");
|
||||
}
|
||||
@ -152,6 +167,10 @@ public class TitleMetaData {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total size of the encrypted content file
|
||||
*/
|
||||
public void setTotalContentSize(){
|
||||
this.totalContentSize = 0;
|
||||
for(int i = 0; i <contents.length-1;i++){
|
||||
@ -159,15 +178,29 @@ public class TitleMetaData {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of all encrypted contents (Without tmd,cetk and h3 files)
|
||||
* @return Size of contents
|
||||
*/
|
||||
public long getTotalContentSize() {
|
||||
return totalContentSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks of the title is an update
|
||||
* @return true if its an update
|
||||
*/
|
||||
public boolean isUpdate() {
|
||||
return (titleID & 0x5000E00000000L) == 0x5000E00000000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads all content files (encrypted)
|
||||
* @param progress: A progress object can be used to get informations of the progress. Will be ignored when null is used.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void downloadContents(Progress progress) throws IOException{
|
||||
|
||||
String tmpPath = getContentPath();
|
||||
File f = new File(tmpPath);
|
||||
if(!f.exists())f.mkdir();
|
||||
@ -182,14 +215,26 @@ public class TitleMetaData {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of the folder where the encrypted content files are stored
|
||||
* @return path of contents
|
||||
*/
|
||||
public String getContentPath() {
|
||||
return nus.getContentPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the NUSTitle of this TMD
|
||||
* @return NUSTitle
|
||||
*/
|
||||
public NUSTitle getNUSTitle() {
|
||||
return nus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the NUSTitle of this TMD
|
||||
* @param nus The NUStitle that will be set
|
||||
*/
|
||||
public void setNUSTitle(NUSTitle nus) {
|
||||
this.nus = nus;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import de.mas.jnustool.Progress;
|
||||
import de.mas.jnustool.ProgressUpdateListener;
|
||||
import de.mas.jnustool.Starter;
|
||||
import de.mas.jnustool.util.NUSTitleInformation;
|
||||
import de.mas.jnustool.util.Settings;
|
||||
|
||||
public class UpdateChooser extends JPanel {
|
||||
/**
|
||||
@ -73,7 +74,7 @@ public class UpdateChooser extends JPanel {
|
||||
tableData[i][3] = n.getLatestVersion();
|
||||
|
||||
JComboBox<String> comboBox = new JComboBox<>();
|
||||
for(String v : n.getAllVersions()){
|
||||
for(String v : n.getAllVersionsAsString()){
|
||||
comboBox.addItem(v);
|
||||
}
|
||||
final int position = i;
|
||||
@ -216,7 +217,11 @@ public class UpdateChooser extends JPanel {
|
||||
public void run() {
|
||||
progressBar_1.setValue(0);
|
||||
progress.clear();
|
||||
Starter.downloadEncrypted(output_,progress);
|
||||
if(Settings.DL_ALL_VERSIONS){
|
||||
Starter.downloadEncryptedAllVersions(output_,progress);
|
||||
}else{
|
||||
Starter.downloadEncrypted(output_,progress);
|
||||
}
|
||||
progress.operationFinish();
|
||||
Logger.messageBox("Finished");
|
||||
}
|
||||
@ -229,7 +234,6 @@ public class UpdateChooser extends JPanel {
|
||||
}
|
||||
});
|
||||
panel.add(btnDownloadEncrypted);
|
||||
|
||||
|
||||
btnNewButton.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
@ -305,6 +309,7 @@ public class UpdateChooser extends JPanel {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* each row TableCellEditor
|
||||
*
|
||||
@ -312,24 +317,27 @@ public class UpdateChooser extends JPanel {
|
||||
* @author Nobuo Tamemasa
|
||||
*/
|
||||
|
||||
class EachRowEditor implements TableCellEditor {
|
||||
protected Hashtable editors;
|
||||
class EachRowEditor implements TableCellEditor {
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected Hashtable editors;
|
||||
|
||||
protected TableCellEditor editor, defaultEditor;
|
||||
|
||||
JTable table;
|
||||
|
||||
/**
|
||||
* Constructs a EachRowEditor. create default editor
|
||||
*
|
||||
* @see TableCellEditor
|
||||
* @see DefaultCellEditor
|
||||
*/
|
||||
public EachRowEditor(JTable table) {
|
||||
this.table = table;
|
||||
editors = new Hashtable();
|
||||
defaultEditor = new DefaultCellEditor(new JTextField());
|
||||
}
|
||||
protected TableCellEditor editor, defaultEditor;
|
||||
|
||||
JTable table;
|
||||
|
||||
/**
|
||||
* Constructs a EachRowEditor. create default editor
|
||||
*
|
||||
* @see TableCellEditor
|
||||
* @see DefaultCellEditor
|
||||
*/
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public EachRowEditor(JTable table) {
|
||||
this.table = table;
|
||||
editors = new Hashtable();
|
||||
defaultEditor = new DefaultCellEditor(new JTextField());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param row
|
||||
@ -337,7 +345,8 @@ public class UpdateChooser extends JPanel {
|
||||
* @param editor
|
||||
* table cell editor
|
||||
*/
|
||||
public void setEditorAt(int row, TableCellEditor editor) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setEditorAt(int row, TableCellEditor editor) {
|
||||
editors.put(new Integer(row), editor);
|
||||
}
|
||||
|
||||
|
@ -78,8 +78,9 @@ public class Downloader {
|
||||
public void downloadTMD(long titleID,int version,String path) throws IOException {
|
||||
String version_suf = "";
|
||||
if(version > 0) version_suf = "." + version;
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" + version_suf;
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" + version_suf;
|
||||
downloadFile(URL, "title.tmd",path,null);
|
||||
if(Settings.DL_ALL_VERSIONS) downloadFile(URL, "title.tmd"+version_suf,path,null);
|
||||
}
|
||||
|
||||
public void downloadFile(String fileURL,String filename,String tmpPath, Progress progress) throws IOException{
|
||||
@ -114,23 +115,28 @@ public class Downloader {
|
||||
public void downloadFile(String fileURL,String filename) throws IOException{
|
||||
downloadFile(fileURL, filename,null,null);
|
||||
}
|
||||
|
||||
public void downloadTicket(long titleID,String path) throws IOException {
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
|
||||
downloadFile(URL, "title.tik",path,null);
|
||||
}
|
||||
|
||||
public void downloadContent(long titleID,int contentID,Progress progress) throws IOException {
|
||||
downloadContent(titleID,contentID, null,progress);
|
||||
}
|
||||
|
||||
public byte[] downloadContentToByteArray(long titleID,int contentID) throws IOException {
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID);
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
|
||||
String version_suf = "";
|
||||
if(version > 0) version_suf = "." + version;
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" +version_suf;
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
private byte[] downloadFileToByteArray(String fileURL) throws IOException {
|
||||
|
||||
int BUFFER_SIZE = 0x800;
|
||||
|
@ -125,7 +125,7 @@ public class NUSTitleInformation implements Comparable<NUSTitleInformation>, Ser
|
||||
for(Integer i :versions){
|
||||
result += ";" + i;
|
||||
}
|
||||
result += ";" + getSelectedVersion();
|
||||
//result += ";" + getSelectedVersion();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -166,7 +166,11 @@ public class NUSTitleInformation implements Comparable<NUSTitleInformation>, Ser
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<String> getAllVersions() {
|
||||
public List<Integer> getAllVersions() {
|
||||
return versions;
|
||||
}
|
||||
|
||||
public List<String> getAllVersionsAsString() {
|
||||
List<String> list = new ArrayList<>();
|
||||
if(versions != null && !versions.isEmpty()){
|
||||
for(Integer v: versions){
|
||||
|
@ -8,5 +8,6 @@ public class Settings {
|
||||
public static boolean skipBrokenFiles = false;
|
||||
public static boolean skipExistingFiles = true;
|
||||
public static boolean skipExistingTMDTICKET = true;
|
||||
|
||||
public static boolean DL_ALL_VERSIONS = false;
|
||||
public static String FILELIST_NAME = "filelist.txt";
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package de.mas.jnustool.util;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
@ -83,4 +86,51 @@ public class Util {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String replaceCharsInString(String path){
|
||||
if(path != null){
|
||||
path = path.replaceAll("[:\\\\*?|<>]", "");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public static void buildFileList(String basePath, String targetPath, String filename) {
|
||||
File contentFolder = new File(basePath + "\\" + targetPath);
|
||||
if(contentFolder.exists()){
|
||||
FileWriter fw;
|
||||
BufferedWriter bw = null;
|
||||
try {
|
||||
fw = new FileWriter(basePath + "\\ "+ filename);
|
||||
bw = new BufferedWriter(fw);
|
||||
bw.write(readFileListRecursive(contentFolder));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
bw.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO make it more generic
|
||||
private static String readFileListRecursive(File contentFolder) {
|
||||
if(contentFolder.exists()){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(File f : contentFolder.listFiles()){
|
||||
if(f.isDirectory()){
|
||||
sb.append("?" + f.getName() + "\n");
|
||||
sb.append(readFileListRecursive(f));
|
||||
sb.append("?..\n");
|
||||
}else{
|
||||
sb.append(f.getName() + "\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}else{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user