Code clean up, made attributes/classes final, added missing private constructors

added proper skip for inputstreams and serveral more small improvements.
Not tested (yet)
This commit is contained in:
Maschell 2016-12-16 16:48:17 +01:00
parent 6b8efcaf53
commit a9b989ee0e
50 changed files with 593 additions and 503 deletions

View File

@ -25,10 +25,10 @@ import de.mas.jnus.lib.utils.StreamUtils;
import de.mas.jnus.lib.utils.Utils;
import de.mas.jnus.lib.utils.cryptography.NUSDecryption;
import lombok.Getter;
import lombok.Setter;
public class DecryptionService {
public final class DecryptionService {
private static Map<NUSTitle,DecryptionService> instances = new HashMap<>();
@Getter private final NUSTitle NUSTitle;
public static DecryptionService getInstance(NUSTitle nustitle) {
if(!instances.containsKey(nustitle)){
@ -37,10 +37,8 @@ public class DecryptionService {
return instances.get(nustitle);
}
@Getter @Setter private NUSTitle NUSTitle = null;
private DecryptionService(NUSTitle nustitle){
setNUSTitle(nustitle);
this.NUSTitle = nustitle;
}
public Ticket getTicket() {
@ -80,16 +78,16 @@ public class DecryptionService {
}
if(targetFile.length() == entry.getFileSize()){
Content c = entry.getContent();
if(!c.isHashed()){
if(c.isHashed()){
System.out.println("File already exists: " + entry.getFilename());
return;
}else{
if(Arrays.equals(HashUtil.hashSHA1(target,(int) c.getEncryptedFileSize()), c.getSHA2Hash())){
System.out.println("File already exists: " + entry.getFilename());
return;
}else{
System.out.println("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename());
}
}else{
System.out.println("File already exists: " + entry.getFilename());
return;
}
}else{
@ -138,9 +136,7 @@ public class DecryptionService {
short contentIndex = (short)content.getIndex();
long encryptedFileSize = content.getEncryptedFileSize();
if(!content.isEncrypted()){
StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream,size,content.getSHA2Hash(),encryptedFileSize);
}else{
if(content.isEncrypted()){
if(content.isHashed()){
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
byte[] h3 = dataProvider.getContentH3Hash(content);
@ -148,6 +144,8 @@ public class DecryptionService {
}else{
nusdecryption.decryptFileStream(inputStream, outputStream, size, (short)contentIndex,content.getSHA2Hash(),encryptedFileSize);
}
}else{
StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream,size,content.getSHA2Hash(),encryptedFileSize);
}
inputStream.close();

View File

@ -12,11 +12,12 @@ import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.Getter;
import lombok.Setter;
public class ExtractionService {
public final class ExtractionService {
private static Map<NUSTitle,ExtractionService> instances = new HashMap<>();
@Getter private final NUSTitle NUSTitle;
public static ExtractionService getInstance(NUSTitle nustitle) {
if(!instances.containsKey(nustitle)){
instances.put(nustitle, new ExtractionService(nustitle));
@ -24,10 +25,8 @@ public class ExtractionService {
return instances.get(nustitle);
}
@Getter @Setter private NUSTitle NUSTitle = null;
private ExtractionService(NUSTitle nustitle){
setNUSTitle(nustitle);
this.NUSTitle = nustitle;
}
private NUSDataProvider getDataProvider(){
@ -76,7 +75,7 @@ public class ExtractionService {
byte[] rawTMD= getDataProvider().getRawTMD();
if(rawTMD != null && rawTMD.length == 0){
if(rawTMD == null || rawTMD.length == 0){
System.out.println("Couldn't write TMD: No TMD loaded");
return;
}
@ -90,7 +89,7 @@ public class ExtractionService {
byte[] rawTicket= getDataProvider().getRawTicket();
if(rawTicket != null && rawTicket.length == 0){
if(rawTicket == null || rawTicket.length == 0){
System.out.println("Couldn't write Ticket: No Ticket loaded");
return;
}
@ -104,7 +103,7 @@ public class ExtractionService {
byte[] rawCert = getDataProvider().getRawCert();
if(rawCert != null && rawCert.length == 0){
if(rawCert == null || rawCert.length == 0){
System.out.println("Couldn't write Cert: No Cert loaded");
return;
}

View File

@ -17,8 +17,6 @@ import lombok.Getter;
import lombok.Setter;
public class NUSTitle {
@Getter @Setter private String inputPath = "";
@Getter @Setter private FST FST;
@Getter @Setter private TMD TMD;
@Getter @Setter private Ticket ticket;
@ -53,10 +51,10 @@ public class NUSTitle {
}
public FSTEntry getFSTEntryByFullPath(String givenFullPath) {
givenFullPath = givenFullPath.replaceAll("/", "\\\\");
if(!givenFullPath.startsWith("\\")) givenFullPath = "\\" +givenFullPath;
String fullPath = givenFullPath.replaceAll("/", "\\\\");
if(!fullPath.startsWith("\\")) fullPath = "\\" +fullPath;
for(FSTEntry f :getAllFSTEntriesFlat()){
if(f.getFullPath().equals(givenFullPath)){
if(f.getFullPath().equals(fullPath)){
return f;
}
}

View File

@ -7,9 +7,9 @@ import lombok.Data;
@Data
public class NUSTitleConfig {
private String inputPath = "";
private WUDInfo WUDInfo = null;
private Ticket ticket = null;
private String inputPath;
private WUDInfo WUDInfo;
private Ticket ticket;
private int version = Settings.LATEST_TMD_VERSION;
private long titleID = 0x0L;

View File

@ -13,16 +13,14 @@ import de.mas.jnus.lib.utils.cryptography.AESDecryption;
abstract class NUSTitleLoader {
protected NUSTitleLoader(){
//should be empty
}
public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception{
NUSTitle result = new NUSTitle();
NUSDataProvider dataProvider = getDataProvider(config);
NUSDataProvider dataProvider = getDataProvider(result, config);
result.setDataProvider(dataProvider);
dataProvider.setNUSTitle(result);
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD());
result.setTMD(tmd);
@ -63,5 +61,5 @@ abstract class NUSTitleLoader {
return result;
}
protected abstract NUSDataProvider getDataProvider(NUSTitleConfig config);
protected abstract NUSDataProvider getDataProvider(NUSTitle title,NUSTitleConfig config);
}

View File

@ -4,7 +4,7 @@ import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.implementations.NUSDataProviderLocal;
import de.mas.jnus.lib.implementations.NUSDataProvider;
public class NUSTitleLoaderLocal extends NUSTitleLoader {
public final class NUSTitleLoaderLocal extends NUSTitleLoader {
private NUSTitleLoaderLocal(){
super();
@ -26,10 +26,8 @@ public class NUSTitleLoaderLocal extends NUSTitleLoader {
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderLocal result = new NUSDataProviderLocal();
result.setLocalPath(config.getInputPath());
return result;
protected NUSDataProvider getDataProvider(NUSTitle title,NUSTitleConfig config) {
return new NUSDataProviderLocal(title,config.getInputPath());
}
}

View File

@ -4,7 +4,7 @@ import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.implementations.NUSDataProviderRemote;
import de.mas.jnus.lib.implementations.NUSDataProvider;
public class NUSTitleLoaderRemote extends NUSTitleLoader{
public final class NUSTitleLoaderRemote extends NUSTitleLoader{
private NUSTitleLoaderRemote(){
super();
@ -33,11 +33,8 @@ public class NUSTitleLoaderRemote extends NUSTitleLoader{
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderRemote result = new NUSDataProviderRemote();
result.setVersion(config.getVersion());
result.setTitleID(config.getTitleID());
return result;
protected NUSDataProvider getDataProvider(NUSTitle title,NUSTitleConfig config) {
return new NUSDataProviderRemote(title,config.getVersion(),config.getTitleID());
}
}

View File

@ -8,7 +8,7 @@ import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfoParser;
public class NUSTitleLoaderWUD extends NUSTitleLoader {
public final class NUSTitleLoaderWUD extends NUSTitleLoader {
private NUSTitleLoaderWUD(){
super();
@ -20,7 +20,7 @@ public class NUSTitleLoaderWUD extends NUSTitleLoader {
public static NUSTitle loadNUSTitle(String WUDPath, byte[] titleKey) throws Exception{
NUSTitleLoader loader = new NUSTitleLoaderWUD();
NUSTitleConfig config = new NUSTitleConfig();
byte[] usedTitleKey = titleKey;
File wudFile = new File(WUDPath);
if(!wudFile.exists()){
System.out.println(WUDPath + " does not exist.");
@ -28,15 +28,15 @@ public class NUSTitleLoaderWUD extends NUSTitleLoader {
}
WUDImage image = new WUDImage(wudFile);
if(titleKey == null){
if(usedTitleKey == null){
File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
if(!keyFile.exists()){
System.out.println(keyFile.getAbsolutePath() + " does not exist and no title key was provided.");
return null;
}
titleKey = Files.readAllBytes(keyFile.toPath());
usedTitleKey = Files.readAllBytes(keyFile.toPath());
}
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), titleKey);
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), usedTitleKey);
if(wudInfo == null){
return null;
}
@ -47,10 +47,8 @@ public class NUSTitleLoaderWUD extends NUSTitleLoader {
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderWUD result = new NUSDataProviderWUD();
result.setWUDInfo(config.getWUDInfo());
return result;
protected NUSDataProvider getDataProvider(NUSTitle title,NUSTitleConfig config) {
return new NUSDataProviderWUD(title,config.getWUDInfo());
}
}

View File

@ -9,7 +9,7 @@ import de.mas.jnus.lib.implementations.woomy.WoomyParser;
import lombok.extern.java.Log;
@Log
public class NUSTitleLoaderWoomy extends NUSTitleLoader {
public final class NUSTitleLoaderWoomy extends NUSTitleLoader {
public static NUSTitle loadNUSTitle(String inputFile) throws Exception{
NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
@ -24,10 +24,8 @@ public class NUSTitleLoaderWoomy extends NUSTitleLoader {
return loader.loadNusTitle(config);
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderWoomy dataProvider = new NUSDataProviderWoomy();
dataProvider.setWoomyInfo(config.getWoomyInfo());
return dataProvider;
protected NUSDataProvider getDataProvider(NUSTitle title,NUSTitleConfig config) {
return new NUSDataProviderWoomy(title,config.getWoomyInfo());
}
}

View File

@ -23,7 +23,11 @@ import de.mas.jnus.lib.utils.Utils;
import lombok.extern.java.Log;
@Log
public class WUDService {
public final class WUDService {
private WUDService(){
//Just an utility class
}
public static File compressWUDToWUX(WUDImage image,String outputFolder) throws IOException{
return compressWUDToWUX(image, outputFolder, "game.wux",false);
}
@ -44,16 +48,17 @@ public class WUDService {
return null;
}
Utils.createDir(outputFolder);
String usedOutputFolder = outputFolder;
if(usedOutputFolder == null) usedOutputFolder = "";
Utils.createDir(usedOutputFolder);
String filePath;
if(outputFolder == null) outputFolder = "";
if(!outputFolder.isEmpty()){
filePath = outputFolder+ File.separator + filename;
}else{
if(usedOutputFolder.isEmpty()){
filePath = filename;
}else{
filePath = usedOutputFolder + File.separator + filename;
}
File outputFile = new File(filePath);
if(outputFile.exists() && !overwrite){
@ -98,14 +103,14 @@ public class WUDService {
int read = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
ByteArrayWrapper hash = new ByteArrayWrapper(HashUtil.hashSHA1(blockBuffer));
if((oldOffset = sectorHashes.get(hash)) != null){
sectorMapping.put(curSector, oldOffset);
oldOffset = null;
}else{ //its a new sector
if((oldOffset = sectorHashes.get(hash)) == null){
sectorMapping.put(curSector, realSector);
sectorHashes.put(hash, realSector);
fileOutput.write(blockBuffer);
realSector++;
}else{
sectorMapping.put(curSector, oldOffset);
oldOffset = null;
}
written += read;

View File

@ -4,39 +4,53 @@ import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.content.ContentInfo;
import lombok.Getter;
import lombok.Setter;
public class TMD {
@Getter @Setter private int signatureType; // 0x000
@Getter @Setter private byte[] signature = new byte[0x100]; // 0x004
@Getter @Setter private byte[] issuer = new byte[0x40]; // 0x140
@Getter @Setter private byte version; // 0x180
@Getter @Setter private byte CACRLVersion; // 0x181
@Getter @Setter private byte signerCRLVersion; // 0x182
@Getter @Setter private long systemVersion; // 0x184
@Getter @Setter private long titleID; // 0x18C
@Getter @Setter private int titleType; // 0x194
@Getter @Setter private short groupID; // 0x198
@Getter @Setter private byte[] reserved = new byte[62]; // 0x19A
@Getter @Setter private int accessRights; // 0x1D8
@Getter @Setter private short titleVersion; // 0x1DC
@Getter @Setter private short contentCount; // 0x1DE
@Getter @Setter private short bootIndex; // 0x1E0
@Getter @Setter private byte[] SHA2 = new byte[0x20]; // 0x1E4
@Getter @Setter private ContentInfo[] contentInfos = new ContentInfo[0x40];
Map<Integer,Content> contentToIndex = new HashMap<>();
Map<Integer,Content> contentToID = new HashMap<>();
@Getter @Setter private byte[] rawTMD = new byte[0];
private TMD(){
public final class TMD {
@Getter private final int signatureType; // 0x000
@Getter private final byte[] signature; // 0x004
@Getter private final byte[] issuer; // 0x140
@Getter private final byte version; // 0x180
@Getter private final byte CACRLVersion; // 0x181
@Getter private final byte signerCRLVersion; // 0x182
@Getter private final long systemVersion; // 0x184
@Getter private final long titleID; // 0x18C
@Getter private final int titleType; // 0x194
@Getter private final short groupID; // 0x198
@Getter private final byte[] reserved; // 0x19A
@Getter private final int accessRights; // 0x1D8
@Getter private final short titleVersion; // 0x1DC
@Getter private final short contentCount; // 0x1DE
@Getter private final short bootIndex; // 0x1E0
@Getter private final byte[] SHA2; // 0x1E4
@Getter private final ContentInfo[] contentInfos;
private final Map<Integer,Content> contentToIndex = new HashMap<>();
private final Map<Integer,Content> contentToID = new HashMap<>();
private TMD(TMDParam param) {
super();
this.signatureType = param.getSignatureType();
this.signature = param.getSignature();
this.issuer = param.getIssuer();
this.version = param.getVersion();
this.CACRLVersion = param.getCACRLVersion();
this.signerCRLVersion = param.getSignerCRLVersion();
this.systemVersion = param.getSystemVersion();
this.titleID = param.getTitleID();
this.titleType = param.getTitleType();
this.groupID = param.getGroupID();
this.reserved = param.getReserved();
this.accessRights = param.getAccessRights();
this.titleVersion = param.getTitleVersion();
this.contentCount = param.getContentCount();
this.bootIndex = param.getBootIndex();
this.SHA2 = param.getSHA2();
this.contentInfos = param.getContentInfos();
}
public static TMD parseTMD(File tmd) throws IOException {
@ -48,15 +62,12 @@ public class TMD {
}
public static TMD parseTMD(byte[] input) {
TMD result = new TMD();
result.setRawTMD(Arrays.copyOf(input,input.length));
byte[] signature = new byte[0x100];
byte[] issuer = new byte[0x40];
byte[] reserved = new byte[62];
byte[] SHA2 = new byte[0x20];
ContentInfo[] contentInfos = result.getContentInfos();
ContentInfo[] contentInfos = new ContentInfo[0x40];
ByteBuffer buffer = ByteBuffer.allocate(input.length);
buffer.put(input);
@ -106,6 +117,25 @@ public class TMD {
contentInfos[i] = ContentInfo.parseContentInfo(contentInfo);
}
TMDParam param = new TMDParam();
param.setSignatureType(signatureType);
param.setSignature(signature);
param.setVersion(version);
param.setCACRLVersion(CACRLVersion);
param.setSignerCRLVersion(signerCRLVersion);
param.setSystemVersion(systemVersion);
param.setTitleID(titleID);
param.setTitleType(titleType);
param.setGroupID(groupID);
param.setAccessRights(accessRights);
param.setTitleVersion(titleVersion);
param.setContentCount(contentCount);
param.setBootIndex(bootIndex);
param.setSHA2(SHA2);
param.setContentInfos(contentInfos);
TMD result = new TMD(param);
//Get Contents
for(int i =0;i<contentCount;i++){
buffer.position(0xB04+(0x30*i));
@ -116,22 +146,6 @@ public class TMD {
result.setContentToID(c.getID(), c);
}
result.setSignatureType(signatureType);
result.setSignature(signature);
result.setVersion(version);
result.setCACRLVersion(CACRLVersion);
result.setSignerCRLVersion(signerCRLVersion);
result.setSystemVersion(systemVersion);
result.setTitleID(titleID);
result.setTitleType(titleType);
result.setGroupID(groupID);
result.setAccessRights(accessRights);
result.setTitleVersion(titleVersion);
result.setContentCount(contentCount);
result.setBootIndex(bootIndex);
result.setSHA2(SHA2);
result.setContentInfos(contentInfos);
return result;
}

View File

@ -0,0 +1,25 @@
package de.mas.jnus.lib.entities;
import de.mas.jnus.lib.entities.content.ContentInfo;
import lombok.Data;
@Data
public class TMDParam {
private int signatureType; // 0x000
private byte[] signature = new byte[0x100]; // 0x004
private byte[] issuer = new byte[0x40]; // 0x140
private byte version; // 0x180
private byte CACRLVersion; // 0x181
private byte signerCRLVersion; // 0x182
private long systemVersion; // 0x184
private long titleID; // 0x18C
private int titleType; // 0x194
private short groupID; // 0x198
private byte[] reserved = new byte[62]; // 0x19A
private int accessRights; // 0x1D8
private short titleVersion; // 0x1DC
private short contentCount; // 0x1DE
private short bootIndex; // 0x1E0
private byte[] SHA2 = new byte[0x20]; // 0x1E4
private ContentInfo[] contentInfos = new ContentInfo[0x40]; //
}

View File

@ -10,23 +10,20 @@ import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.utils.Utils;
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class Ticket {
@Getter @Setter private byte[] encryptedKey = new byte[0x10];
@Getter @Setter private byte[] decryptedKey = new byte[0x10];
public final class Ticket {
@Getter private final byte[] encryptedKey;
@Getter private final byte[] decryptedKey;
@Getter @Setter private byte[] IV = new byte[0x10];
@Getter @Setter private byte[] cert0 = new byte[0x300];
@Getter @Setter private byte[] cert1 = new byte[0x400];
@Getter @Setter private byte[] rawTicket = new byte[0];
private Ticket(){
@Getter private final byte[] IV;
private Ticket(byte[] encryptedKey, byte[] decryptedKey,byte[] IV) {
super();
this.encryptedKey = encryptedKey;
this.decryptedKey = decryptedKey;
this.IV = IV;
}
public static Ticket parseTicket(File ticket) throws IOException {
@ -55,36 +52,15 @@ public class Ticket {
long titleID = buffer.getLong();
Ticket result = createTicket(encryptedKey,titleID);
result.setRawTicket(Arrays.copyOf(ticket, ticket.length));
//read certs.
byte[] cert0 = new byte[0x300];
byte[] cert1 = new byte[0x400];
if(ticket.length >= 0x650){
buffer.position(0x350);
buffer.get(cert0,0x00,0x300);
}
if(ticket.length >= 0xA50){
buffer.position(0x650);
buffer.get(cert1,0x00,0x400);
}
result.setCert0(cert0);
result.setCert1(cert1);
return result;
}
public static Ticket createTicket(byte[] encryptedKey, long titleID) {
Ticket result = new Ticket();
result.encryptedKey = encryptedKey;
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
result.decryptedKey = calculateDecryptedKey(result.encryptedKey,IV);
result.setIV(IV);
byte[] decryptedKey = calculateDecryptedKey(encryptedKey,IV);
return result;
return new Ticket(encryptedKey,decryptedKey,IV);
}
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
@ -109,9 +85,7 @@ public class Ticket {
if (getClass() != obj.getClass())
return false;
Ticket other = (Ticket) obj;
if (!Arrays.equals(encryptedKey, other.encryptedKey))
return false;
return true;
return Arrays.equals(encryptedKey, other.encryptedKey);
}
@Override

View File

@ -20,16 +20,24 @@ public class Content{
public static final short CONTENT_HASHED = 0x0002;
public static final short CONTENT_ENCRYPTED = 0x0001;
@Getter @Setter private int ID = 0x00;
@Getter @Setter private short index = 0x00;
@Getter @Setter private short type = 0x0000;
@Getter private final int ID;
@Getter private final short index;
@Getter private final short type;
@Getter @Setter private long encryptedFileSize = 0;
@Getter @Setter private byte[] SHA2Hash = new byte[0x14];
@Getter private final long encryptedFileSize;
@Getter private final byte[] SHA2Hash;
@Getter private List<FSTEntry> entries = new ArrayList<>();
@Getter private final List<FSTEntry> entries = new ArrayList<>();
@Getter @Setter private ContentFSTInfo contentFSTInfo = null;
@Getter @Setter private ContentFSTInfo contentFSTInfo;
private Content(ContentParam param) {
this.ID = param.getID();
this.index = param.getIndex();
this.type = param.getType();
this.encryptedFileSize = param.getEncryptedFileSize();
this.SHA2Hash = param.getSHA2Hash();
}
/**
* Creates a new Content object given be the raw byte data
@ -44,6 +52,7 @@ public class Content{
ByteBuffer buffer = ByteBuffer.allocate(input.length);
buffer.put(input);
buffer.position(0);
int ID = buffer.getInt(0x00);
short index = buffer.getShort(0x04);
short type = buffer.getShort(0x06);
@ -51,15 +60,15 @@ public class Content{
buffer.position(0x10);
byte[] hash = new byte[0x14];
buffer.get(hash, 0x00, 0x14);
return new Content(ID, index,type,encryptedFileSize, hash);
}
public Content(int ID, short index, short type, long encryptedFileSize, byte[] hash) {
setID(ID);
setIndex(index);
setType(type);
setEncryptedFileSize(encryptedFileSize);
setSHA2Hash(hash);
ContentParam param = new ContentParam();
param.setID(ID);
param.setIndex(index);
param.setType(type);
param.setEncryptedFileSize(encryptedFileSize);
param.setSHA2Hash(hash);
return new Content(param);
}
/**
@ -137,9 +146,7 @@ public class Content{
Content other = (Content) obj;
if (ID != other.ID)
return false;
if (!Arrays.equals(SHA2Hash, other.SHA2Hash))
return false;
return true;
return Arrays.equals(SHA2Hash, other.SHA2Hash);
}
@Override

View File

@ -4,7 +4,6 @@ import java.nio.ByteBuffer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@EqualsAndHashCode
/**
@ -13,20 +12,23 @@ import lombok.Setter;
* @author Maschell
*
*/
public class ContentFSTInfo {
@Getter @Setter private long offsetSector;
@Getter @Setter private long sizeSector;
@Getter @Setter private long ownerTitleID;
@Getter @Setter private int groupID;
@Getter @Setter private byte unkown;
public final class ContentFSTInfo {
@Getter private final long offsetSector;
@Getter private final long sizeSector;
@Getter private final long ownerTitleID;
@Getter private final int groupID;
@Getter private final byte unkown;
private static int SECTOR_SIZE = 0x8000;
private ContentFSTInfo(){
private ContentFSTInfo(ContentFSTInfoParam param){
this.offsetSector = param.getOffsetSector();
this.sizeSector = param.getSizeSector();
this.ownerTitleID = param.getOwnerTitleID();
this.groupID = param.getGroupID();
this.unkown = param.getUnkown();
}
/**
* Creates a new ContentFSTInfo object given be the raw byte data
* @param input 0x20 byte of data from the FST (starting at 0x20)
@ -37,7 +39,7 @@ public class ContentFSTInfo {
System.out.println("Error: invalid ContentFSTInfo byte[] input");
return null;
}
ContentFSTInfo cFSTInfo = new ContentFSTInfo();
ContentFSTInfoParam param = new ContentFSTInfoParam();
ByteBuffer buffer = ByteBuffer.allocate(input.length);
buffer.put(input);
@ -48,13 +50,13 @@ public class ContentFSTInfo {
int groupID = buffer.getInt();
byte unkown = buffer.get();
cFSTInfo.setOffsetSector(offset);
cFSTInfo.setSizeSector(size);
cFSTInfo.setOwnerTitleID(ownerTitleID);
cFSTInfo.setGroupID(groupID);
cFSTInfo.setUnkown(unkown);
param.setOffsetSector(offset);
param.setSizeSector(size);
param.setOwnerTitleID(ownerTitleID);
param.setGroupID(groupID);
param.setUnkown(unkown);
return cFSTInfo;
return new ContentFSTInfo(param);
}
/**

View File

@ -0,0 +1,12 @@
package de.mas.jnus.lib.entities.content;
import lombok.Data;
@Data
public class ContentFSTInfoParam {
private long offsetSector = 0;
private long sizeSector = 0;
private long ownerTitleID = 0;
private int groupID = 0;
private byte unkown = 0;
}

View File

@ -5,7 +5,6 @@ import java.util.Arrays;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@EqualsAndHashCode
/**
@ -14,9 +13,9 @@ import lombok.Setter;
*
*/
public class ContentInfo{
@Getter @Setter private short indexOffset = 0x00;
@Getter @Setter private short commandCount = 0x00;
@Getter @Setter private byte[] SHA2Hash = new byte[0x20];
@Getter private final short indexOffset;
@Getter private final short commandCount;
@Getter private final byte[] SHA2Hash;
public ContentInfo() {
this((short) 0);
@ -29,9 +28,9 @@ public class ContentInfo{
this(indexOffset,commandCount,null);
}
public ContentInfo(short indexOffset,short commandCount,byte[] SHA2Hash) {
setIndexOffset(indexOffset);
setCommandCount(commandCount);
setSHA2Hash(SHA2Hash);
this.indexOffset = indexOffset;
this.commandCount = commandCount;
this.SHA2Hash = SHA2Hash;
}
/**

View File

@ -0,0 +1,14 @@
package de.mas.jnus.lib.entities.content;
import lombok.Data;
@Data
public class ContentParam {
private int ID = 0x00;
private short index = 0x00;
private short type = 0x0000;
private long encryptedFileSize = 0;
private byte[] SHA2Hash = new byte[0x14];
private ContentFSTInfo contentFSTInfo = null;
}

View File

@ -8,22 +8,23 @@ import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
import de.mas.jnus.lib.utils.ByteUtils;
import lombok.Getter;
import lombok.Setter;
/**
* Represents the FST
* @author Maschell
*
*/
public class FST {
@Getter @Setter private FSTEntry root = FSTEntry.getRootFSTEntry();
public final class FST {
@Getter private final FSTEntry root = FSTEntry.getRootFSTEntry();
@Getter @Setter private int unknown;
@Getter @Setter private int contentCount = 0;
@Getter private final int unknown;
@Getter private final int contentCount;
@Getter @Setter private Map<Integer,ContentFSTInfo> contentFSTInfos = new HashMap<>();
private FST(){
@Getter private final Map<Integer,ContentFSTInfo> contentFSTInfos = new HashMap<>();
private FST(int unknown, int contentCount) {
super();
this.unknown = unknown;
this.contentCount = contentCount;
}
/**
@ -36,11 +37,12 @@ public class FST {
if(!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[]{0x46,0x53,0x54})){
throw new IllegalArgumentException("Not a FST. Maybe a wrong key?");
}
FST result = new FST();
int unknownValue = ByteUtils.getIntFromBytes(fstData, 0x04);
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
FST result = new FST(unknownValue,contentCount);
int contentfst_offset = 0x20;
int contentfst_size = 0x20*contentCount;
@ -71,11 +73,6 @@ public class FST {
FSTService.parseFST(root,fstSection,nameSection,contentsMappedByIndex,contentFSTInfos);
result.setContentCount(contentCount);
result.setUnknown(unknownValue);
result.setContentFSTInfos(contentFSTInfos);
return result;
}
}

View File

@ -5,7 +5,6 @@ import java.util.List;
import de.mas.jnus.lib.entities.content.Content;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
@ -18,29 +17,38 @@ public class FSTEntry{
public static final byte FSTEntry_DIR = (byte)0x01;
public static final byte FSTEntry_notInNUS = (byte)0x80;
@Getter @Setter private String filename = "";
@Getter @Setter private String path = "";
@Getter @Setter private FSTEntry parent = null;
@Getter private final String filename;
@Getter private final String path;
@Getter private final FSTEntry parent;
private List<FSTEntry> children = null;
@Getter private final List<FSTEntry> children = new ArrayList<>();
@Getter @Setter private short flags;
@Getter private final short flags;
@Getter @Setter private long fileSize = 0;
@Getter @Setter private long fileOffset = 0;
@Getter private final long fileSize;
@Getter private final long fileOffset;
@Getter private Content content = null;
@Getter private final Content content;
@Getter @Setter private byte[] hash = new byte[0x14];
@Getter private final boolean isDir;
@Getter private final boolean isRoot;
@Getter private final boolean isNotInPackage;
@Getter @Setter private boolean isDir = false;
@Getter @Setter private boolean isRoot = false;
@Getter @Setter private boolean notInPackage = false;
@Getter @Setter private short contentFSTID = 0;
public FSTEntry(){
@Getter private final short contentFSTID;
protected FSTEntry(FSTEntryParam fstParam){
this.filename = fstParam.getFilename();
this.path = fstParam.getPath();
this.flags = fstParam.getFlags();
this.parent = fstParam.getParent();
this.fileSize = fstParam.getFileSize();
this.fileOffset = fstParam.getFileOffset();
this.content = fstParam.getContent();
content.addEntry(this);
this.isDir = fstParam.isDir();
this.isRoot = fstParam.isRoot();
this.isNotInPackage = fstParam.isNotInPackage();
this.contentFSTID = fstParam.getContentFSTID();
}
/**
@ -48,9 +56,10 @@ public class FSTEntry{
* @return
*/
public static FSTEntry getRootFSTEntry(){
FSTEntry entry = new FSTEntry();
entry.setRoot(true);
return entry;
FSTEntryParam param = new FSTEntryParam();
param.setRoot(true);
param.setDir(true);
return new FSTEntry(param);
}
public String getFullPath() {
@ -65,18 +74,6 @@ public class FSTEntry{
return count;
}
public void addChildren(FSTEntry fstEntry) {
getChildren().add(fstEntry);
fstEntry.setParent(this);
}
public List<FSTEntry> getChildren() {
if(children == null){
children = new ArrayList<>();
}
return children;
}
public List<FSTEntry> getDirChildren(){
return getDirChildren(false);
}
@ -121,16 +118,6 @@ public class FSTEntry{
return entries;
}
public void setContent(Content content) {
if(content == null){
log.warning("Can't set content for "+ getFilename() + ": Content it null");
System.out.println();
return;
}
this.content = content;
content.addEntry(this);
}
public long getFileOffsetBlock() {
if(getContent().isHashed()){
return (getFileOffset()/0xFC00) * 0x10000;
@ -160,6 +147,6 @@ public class FSTEntry{
public String toString() {
return "FSTEntry [filename=" + filename + ", path=" + path + ", flags=" + flags + ", filesize=" + fileSize
+ ", fileoffset=" + fileOffset + ", content=" + content + ", isDir=" + isDir + ", isRoot=" + isRoot
+ ", notInPackage=" + notInPackage + "]";
+ ", notInPackage=" + isNotInPackage + "]";
}
}

View File

@ -0,0 +1,24 @@
package de.mas.jnus.lib.entities.fst;
import de.mas.jnus.lib.entities.content.Content;
import lombok.Data;
@Data
public class FSTEntryParam {
private String filename = "";
private String path = "";
private FSTEntry parent = null;
private short flags;
private long fileSize = 0;
private long fileOffset = 0;
private Content content = null;
private boolean isDir = false;
private boolean isRoot = false;
private boolean notInPackage = false;
private short contentFSTID = 0;
}

View File

@ -11,8 +11,12 @@ import de.mas.jnus.lib.utils.ByteUtils;
import lombok.extern.java.Log;
@Log
public class FSTService {
protected static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection,Map<Integer,Content> contentsByIndex,Map<Integer,ContentFSTInfo> contentsFSTByIndex) {
public final class FSTService {
private FSTService(){
//Don't use this!!!
}
public static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection,Map<Integer,Content> contentsByIndex,Map<Integer,ContentFSTInfo> contentsFSTByIndex) {
int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
int level = 0;
@ -20,8 +24,6 @@ public class FSTService {
int[] Entry = new int[16];
HashMap<Integer,FSTEntry> fstEntryToOffsetMap = new HashMap<>();
rootEntry.setDir(true);
Entry[level] = 0;
LEntry[level++] = 0;
@ -35,7 +37,7 @@ public class FSTService {
byte[] curEntry = Arrays.copyOfRange(fstSection,i*0x10,(i+1)*0x10);
FSTEntry entry = new FSTEntry();
FSTEntryParam entryParam = new FSTEntryParam();
String path = getFullPath(level, fstSection, namesSection, Entry);
String filename = getName(curEntry,namesSection);
@ -47,19 +49,15 @@ public class FSTService {
short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
if((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS){
entry.setNotInPackage(true);
entryParam.setNotInPackage(true);
}
FSTEntry parent = null;
if((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR){
entry.setDir(true);
entryParam.setDir(true);
int parentOffset = (int) fileOffset;
int nextOffset = (int) fileSize;
FSTEntry parent = fstEntryToOffsetMap.get(parentOffset);
if(parent != null){
log.fine("no parent found for a FSTEntry");
parent.addChildren(entry);
}
parent = fstEntryToOffsetMap.get(parentOffset);
Entry[level] = i;
LEntry[level++] = nextOffset ;
@ -69,26 +67,21 @@ public class FSTService {
break;
}
}else{
entry.setFileOffset(fileOffset<<5);
entry.setFileSize(fileSize);
FSTEntry parent = fstEntryToOffsetMap.get(Entry[level-1]);
if(parent != null){
parent.addChildren(entry);
}else{
log.warning(entryOffset +"couldn't find parent @ " + Entry[level-1]);
}
entryParam.setFileOffset(fileOffset<<5);
entryParam.setFileSize(fileSize);
parent = fstEntryToOffsetMap.get(Entry[level-1]);
}
entry.setFlags(flags);
entry.setFilename(filename);
entry.setPath(path);
entryParam.setFlags(flags);
entryParam.setFilename(filename);
entryParam.setPath(path);
if(contentsByIndex != null){
Content content = contentsByIndex.get((int)contentIndex);
if(content == null){
log.warning("Content for FST Entry not found");
}else{
entry.setContent(content);
entryParam.setContent(content);
ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int)contentIndex);
if(contentFSTInfo == null){
@ -99,7 +92,10 @@ public class FSTService {
}
}
entry.setContentFSTID(contentIndex);
entryParam.setContentFSTID(contentIndex);
entryParam.setParent(parent);
FSTEntry entry = new FSTEntry(entryParam);
fstEntryToOffsetMap.put(entryOffset, entry);
}

View File

@ -3,7 +3,6 @@ package de.mas.jnus.lib.implementations;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.SynchronousQueue;
import com.sun.istack.internal.NotNull;
@ -13,7 +12,6 @@ import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
@ -24,10 +22,10 @@ import lombok.extern.java.Log;
*
*/
public abstract class NUSDataProvider {
@Getter private final NUSTitle NUSTitle;
@Getter @Setter private NUSTitle NUSTitle = null;
public NUSDataProvider () {
public NUSDataProvider (NUSTitle title) {
this.NUSTitle = title;
}
/**
@ -56,7 +54,7 @@ public abstract class NUSDataProvider {
return;
}
byte[] hash = getContentH3Hash(content);
if(hash == null){
if(hash == null || hash.length == 0){
return;
}
String h3Filename = String.format("%08X%s", content.getID(),Settings.H3_EXTENTION);

View File

@ -6,15 +6,18 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import de.mas.jnus.lib.NUSTitle;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.StreamUtils;
import lombok.Getter;
import lombok.Setter;
public class NUSDataProviderLocal extends NUSDataProvider {
@Getter @Setter private String localPath = "";
public final class NUSDataProviderLocal extends NUSDataProvider {
@Getter private final String localPath;
public NUSDataProviderLocal() {
public NUSDataProviderLocal(NUSTitle nustitle, String localPath) {
super(nustitle);
this.localPath = localPath;
}
public String getFilePathOnDisk(Content c) {
@ -29,7 +32,7 @@ public class NUSDataProviderLocal extends NUSDataProvider {
return null;
}
InputStream in = new FileInputStream(filepath);
in.skip(offset);
StreamUtils.skipExactly(in,offset);
return in;
}
@ -38,7 +41,7 @@ public class NUSDataProviderLocal extends NUSDataProvider {
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
File h3File = new File(h3Path);
if(!h3File.exists()){
return null;
return new byte[0];
}
return Files.readAllBytes(h3File.toPath());
}
@ -59,10 +62,6 @@ public class NUSDataProviderLocal extends NUSDataProvider {
return Files.readAllBytes(ticketFile.toPath());
}
@Override
public void cleanup() throws IOException {
}
@Override
public byte[] getRawCert() throws IOException {
String inputPath = getLocalPath();
@ -70,4 +69,9 @@ public class NUSDataProviderLocal extends NUSDataProvider {
File certFile = new File(certPath);
return Files.readAllBytes(certFile.toPath());
}
@Override
public void cleanup() throws IOException {
//We don't need this
}
}

View File

@ -3,21 +3,25 @@ package de.mas.jnus.lib.implementations;
import java.io.IOException;
import java.io.InputStream;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.NUSTitle;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.download.NUSDownloadService;
import lombok.Getter;
import lombok.Setter;
public class NUSDataProviderRemote extends NUSDataProvider {
@Getter @Setter private int version = Settings.LATEST_TMD_VERSION;
@Getter @Setter private long titleID = 0L;
@Getter private final int version;
@Getter private final long titleID;
public NUSDataProviderRemote(NUSTitle title,int version, long titleID) {
super(title);
this.version = version;
this.titleID = titleID;
}
@Override
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
InputStream in = downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock);
return in;
return downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock);
}
private String getRemoteURL(Content content) {
@ -51,12 +55,12 @@ public class NUSDataProviderRemote extends NUSDataProvider {
}
@Override
public void cleanup() throws IOException {
// TODO Auto-generated method stub
public byte[] getRawCert() throws IOException {
return new byte[0];
}
@Override
public byte[] getRawCert() throws IOException {
return null;
public void cleanup() throws IOException {
//We don't need this
}
}

View File

@ -3,6 +3,7 @@ package de.mas.jnus.lib.implementations;
import java.io.IOException;
import java.io.InputStream;
import de.mas.jnus.lib.NUSTitle;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.TMD;
import de.mas.jnus.lib.entities.content.Content;
@ -16,12 +17,13 @@ import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class NUSDataProviderWUD extends NUSDataProvider {
@Getter @Setter private WUDInfo WUDInfo = null;
@Getter private final WUDInfo WUDInfo;
@Setter(AccessLevel.PRIVATE) private TMD TMD = null;
public NUSDataProviderWUD() {
super();
public NUSDataProviderWUD(NUSTitle title,WUDInfo wudinfo) {
super(title);
this.WUDInfo = wudinfo;
}
public long getOffsetInWUD(Content content) {
@ -97,6 +99,7 @@ public class NUSDataProviderWUD extends NUSDataProvider {
@Override
public void cleanup() {
//We don't need it
}
}

View File

@ -5,10 +5,12 @@ import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import de.mas.jnus.lib.NUSTitle;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
import de.mas.jnus.lib.implementations.woomy.WoomyZipFile;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@ -16,8 +18,13 @@ import lombok.extern.java.Log;
@Log
public class NUSDataProviderWoomy extends NUSDataProvider{
@Getter @Setter private WoomyInfo woomyInfo;
@Setter private WoomyZipFile woomyZipFile;
@Getter private final WoomyInfo woomyInfo;
@Setter(AccessLevel.PRIVATE) private WoomyZipFile woomyZipFile;
public NUSDataProviderWoomy(NUSTitle title,WoomyInfo woomyInfo) {
super(title);
this.woomyInfo = woomyInfo;
}
@Override
public InputStream getInputStreamFromContent(@NonNull Content content, long fileOffsetBlock) throws IOException {
@ -39,7 +46,7 @@ public class NUSDataProviderWoomy extends NUSDataProvider{
zipFile.close();
return result;
}
return null;
return new byte[0];
}
@Override
@ -70,10 +77,10 @@ public class NUSDataProviderWoomy extends NUSDataProvider{
}
public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException {
if(woomyZipFile == null || woomyZipFile.isClosed()){
woomyZipFile = getNewWoomyZipFile();
if(this.woomyZipFile == null || this.woomyZipFile.isClosed()){
this.woomyZipFile = getNewWoomyZipFile();
}
return woomyZipFile;
return this.woomyZipFile;
}
private WoomyZipFile getNewWoomyZipFile() throws ZipException, IOException {
@ -82,14 +89,13 @@ public class NUSDataProviderWoomy extends NUSDataProvider{
@Override
public void cleanup() throws IOException {
if(woomyZipFile != null && woomyZipFile.isClosed()){
woomyZipFile.close();
if(this.woomyZipFile != null && this.woomyZipFile.isClosed()){
this.woomyZipFile.close();
}
}
@Override
public byte[] getRawCert() throws IOException {
// TODO Auto-generated method stub
return null;
return new byte[0];
}
}

View File

@ -7,33 +7,19 @@ import lombok.Data;
@Data
public class WoomyMeta {
private String name;
private int icon;
private List<WoomyEntry> entries;
private final String name;
private final int icon;
private final List<WoomyEntry> entries = new ArrayList<>();
public void addEntry(String name,String folder, int entryCount){
WoomyEntry entry = new WoomyEntry(name, folder, entryCount);
getEntries().add(entry);
}
public List<WoomyEntry> getEntries(){
if(entries == null){
setEntries(new ArrayList<>());
}
return entries;
}
@Data
public class WoomyEntry {
public WoomyEntry(String name, String folder, int entryCount) {
setName(name);
setFolder(folder);
setEntryCount(entryCount);
}
private String name;
private String folder;
private int entryCount;
private final String name;
private final String folder;
private final int entryCount;
}
}

View File

@ -9,7 +9,7 @@ import de.mas.jnus.lib.utils.XMLParser;
import lombok.extern.java.Log;
@Log
public class WoomyMetaParser extends XMLParser{
public final class WoomyMetaParser extends XMLParser{
private static final String WOOMY_METADATA_NAME = "name";
private static final String WOOMY_METADATA_ICON = "icon";
@ -25,7 +25,8 @@ public class WoomyMetaParser extends XMLParser{
public static WoomyMeta parseMeta(InputStream data){
XMLParser parser = new WoomyMetaParser();
WoomyMeta result = new WoomyMeta();
String resultName = "";
int resultIcon = 0;
try {
parser.loadDocument(data);
} catch(Exception e){
@ -35,14 +36,17 @@ public class WoomyMetaParser extends XMLParser{
String name = parser.getValueOfElement(WOOMY_METADATA_NAME);
if(name != null && !name.isEmpty()){
result.setName(name);
resultName = name;
}
String icon = parser.getValueOfElement(WOOMY_METADATA_ICON);
if(icon != null && !icon.isEmpty()){
int icon_val = Integer.parseInt(icon);
result.setIcon(icon_val);
resultIcon = icon_val;
}
WoomyMeta result = new WoomyMeta(resultName,resultIcon);
Node entries_node = parser.getNodeByValue(WOOMY_METADATA_ENTRIES);
NodeList entry_list = entries_node.getChildNodes();

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -27,7 +28,10 @@ import lombok.extern.java.Log;
*
*/
@Log
public class WoomyParser {
public final class WoomyParser {
private WoomyParser(){
//
}
public static WoomyInfo createWoomyInfo(File woomyFile) throws IOException, ParserConfigurationException, SAXException{
WoomyInfo result = new WoomyInfo();
if(!woomyFile.exists()){
@ -72,7 +76,7 @@ public class WoomyParser {
if(matcher.matches()){
String[] tokens = entryName.split("[\\\\|/]"); //We only want the filename!
String filename = tokens[tokens.length - 1];
result.put(filename.toLowerCase(), entry);
result.put(filename.toLowerCase(Locale.ENGLISH), entry);
}
}
}

View File

@ -20,13 +20,14 @@ import lombok.extern.java.Log;
public class WUDImage {
public static long WUD_FILESIZE = 0x5D3A00000L;
@Getter @Setter private File fileHandle = null;
@Getter private final File fileHandle;
@Getter @Setter private WUDImageCompressedInfo compressedInfo = null;
@Getter @Setter private boolean isCompressed = false;
@Getter @Setter private boolean isSplitted = false;
@Getter private final boolean isCompressed;
@Getter private final boolean isSplitted;
private long inputFileSize = 0L;
@Setter private WUDDiscReader WUDDiscReader = null;
@Getter private final WUDDiscReader WUDDiscReader;
public WUDImage(File file) throws IOException{
if(file == null || !file.exists()){
@ -42,7 +43,8 @@ public class WUDImage {
if(compressedInfo.isWUX()){
log.info("Image is compressed");
setCompressed(true);
this.isCompressed = true;
this.isSplitted = false;
Map<Integer,Long> indexTable = new HashMap<>();
long offsetIndexTable = compressedInfo.getOffsetIndexTable();
fileStream.seek(offsetIndexTable);
@ -56,27 +58,27 @@ public class WUDImage {
}
compressedInfo.setIndexTable(indexTable);
setCompressedInfo(compressedInfo);
}else if(file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) &&
}else{
this.isCompressed = false;
if(file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) &&
(file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)){
setSplitted(true);
this.isSplitted = true;
log.info("Image is splitted");
}else{
this.isSplitted = false;
}
}
if(isCompressed()){
this.WUDDiscReader = new WUDDiscReaderCompressed(this);
}else if(isSplitted()){
this.WUDDiscReader = new WUDDiscReaderSplitted(this);
}else{
this.WUDDiscReader = new WUDDiscReaderUncompressed(this);
}
fileStream.close();
setFileHandle(file);
}
public WUDDiscReader getWUDDiscReader() {
if(WUDDiscReader == null){
if(isCompressed()){
setWUDDiscReader(new WUDDiscReaderCompressed(this));
}else if(isSplitted()){
setWUDDiscReader(new WUDDiscReaderSplitted(this));
}else{
setWUDDiscReader(new WUDDiscReaderUncompressed(this));
}
}
return WUDDiscReader;
this.fileHandle = file;
}
public long getWUDFileSize() {

View File

@ -15,16 +15,16 @@ public class WUDImageCompressedInfo {
public static final int WUX_MAGIC_1 = 0x1099d02e;
public static final int SECTOR_SIZE = 0x8000;
@Getter @Setter private int magic0;
@Getter @Setter private int magic1;
@Getter @Setter private int sectorSize;
@Getter @Setter private long uncompressedSize;
@Getter @Setter private int flags;
@Getter private final int sectorSize;
@Getter private final long uncompressedSize;
@Getter private final int flags;
@Getter @Setter private long indexTableEntryCount = 0;
@Getter @Setter private long offsetIndexTable = 0;
@Getter @Setter private long offsetSectorArray = 0;
@Getter @Setter private long indexTableSize = 0;
@Getter @Setter private long indexTableEntryCount;
@Getter private final long offsetIndexTable = WUX_HEADER_SIZE;
@Getter @Setter private long offsetSectorArray;
@Getter @Setter private long indexTableSize;
private final boolean valid;
@Getter private Map<Integer,Long> indexTable = new HashMap<>();
@ -33,11 +33,16 @@ public class WUDImageCompressedInfo {
System.out.println("WUX header length wrong");
System.exit(1);
}
setMagic0(ByteUtils.getIntFromBytes(headData, 0x00,ByteOrder.LITTLE_ENDIAN));
setMagic1(ByteUtils.getIntFromBytes(headData, 0x04,ByteOrder.LITTLE_ENDIAN));
setSectorSize(ByteUtils.getIntFromBytes(headData, 0x08,ByteOrder.LITTLE_ENDIAN));
setFlags(ByteUtils.getIntFromBytes(headData, 0x0C,ByteOrder.LITTLE_ENDIAN));
setUncompressedSize(ByteUtils.getLongFromBytes(headData, 0x10,ByteOrder.LITTLE_ENDIAN));
int magic0 = ByteUtils.getIntFromBytes(headData, 0x00,ByteOrder.LITTLE_ENDIAN);
int magic1 = ByteUtils.getIntFromBytes(headData, 0x04,ByteOrder.LITTLE_ENDIAN);
if(magic0 == WUX_MAGIC_0 && magic1 == WUX_MAGIC_1){
valid = true;
}else{
valid = false;
}
this.sectorSize = ByteUtils.getIntFromBytes(headData, 0x08,ByteOrder.LITTLE_ENDIAN);
this.flags = ByteUtils.getIntFromBytes(headData, 0x0C,ByteOrder.LITTLE_ENDIAN);
this.uncompressedSize = ByteUtils.getLongFromBytes(headData, 0x10,ByteOrder.LITTLE_ENDIAN);
calculateOffsets();
}
@ -47,17 +52,16 @@ public class WUDImageCompressedInfo {
}
public WUDImageCompressedInfo(int sectorSize,int flags, long uncompressedSize) {
setMagic0(WUX_MAGIC_0);
setMagic1(WUX_MAGIC_1);
setSectorSize(sectorSize);
setFlags(flags);
setUncompressedSize(uncompressedSize);
this.sectorSize = sectorSize;
this.flags = flags;
this.uncompressedSize= uncompressedSize;
valid = true;
calculateOffsets();
}
private void calculateOffsets() {
long indexTableEntryCount = (getUncompressedSize()+ getSectorSize()-1) / getSectorSize();
setIndexTableEntryCount(indexTableEntryCount);
setOffsetIndexTable(0x20);
long offsetSectorArray = (getOffsetIndexTable() + ((long)getIndexTableEntryCount() * 0x04L));
// align to SECTOR_SIZE
offsetSectorArray = (offsetSectorArray + (long)(getSectorSize()-1));
@ -68,15 +72,7 @@ public class WUDImageCompressedInfo {
}
public boolean isWUX() {
return (getMagic0() == WUX_MAGIC_0 && getMagic1() == WUX_MAGIC_1);
}
@Override
public String toString() {
return "WUDImageCompressedInfo [magic0=" + String.format("0x%08X", magic0) + ", magic1=" + String.format("0x%08X", magic1) + ", sectorSize=" + String.format("0x%08X", sectorSize)
+ ", uncompressedSize=" + String.format("0x%016X", uncompressedSize) + ", flags=" + String.format("0x%08X", flags) + ", indexTableEntryCount="
+ indexTableEntryCount + ", offsetIndexTable=" + offsetIndexTable + ", offsetSectorArray="
+ offsetSectorArray + ", indexTableSize=" + indexTableSize + "]";
return valid;
}
public long getSectorIndex(int sectorIndex) {
@ -90,8 +86,8 @@ public class WUDImageCompressedInfo {
public byte[] getHeaderAsBytes() {
ByteBuffer result = ByteBuffer.allocate(WUX_HEADER_SIZE);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(getMagic0());
result.putInt(getMagic1());
result.putInt(WUX_MAGIC_0);
result.putInt(WUX_MAGIC_1);
result.putInt(getSectorSize());
result.putInt(getFlags());
result.putLong(getUncompressedSize());

View File

@ -6,7 +6,15 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper=true)
public class WUDGamePartition extends WUDPartition {
private byte[] rawTMD;
private byte[] rawCert;
private byte[] rawTicket;
private final byte[] rawTMD;
private final byte[] rawCert;
private final byte[] rawTicket;
public WUDGamePartition(String partitionName, long partitionOffset, byte[] rawTMD, byte[] rawCert,
byte[] rawTicket) {
super(partitionName, partitionOffset);
this.rawTMD = rawTMD;
this.rawCert = rawCert;
this.rawTicket = rawTicket;
}
}

View File

@ -1,5 +1,6 @@
package de.mas.jnus.lib.implementations.wud.parser;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@ -11,19 +12,20 @@ import lombok.Setter;
@Data
public class WUDInfo {
private byte[] titleKey = null;
private final byte[] titleKey;
private WUDDiscReader WUDDiscReader = null;
private Map<String,WUDPartition> partitions = null;
private final WUDDiscReader WUDDiscReader;
private final Map<String,WUDPartition> partitions = new HashMap<>();
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PROTECTED)
private String gamePartitionName;
WUDInfo(){
private WUDGamePartition cachedGamePartition = null;
public void addPartion(String partitionName, WUDGamePartition partition){
getPartitions().put(partitionName, partition);
}
private WUDGamePartition cachedGamePartition = null;
public WUDGamePartition getGamePartition(){
if(cachedGamePartition == null){
cachedGamePartition = findGamePartition();

View File

@ -13,12 +13,11 @@ import de.mas.jnus.lib.entities.fst.FST;
import de.mas.jnus.lib.entities.fst.FSTEntry;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
import de.mas.jnus.lib.utils.ByteUtils;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.extern.java.Log;
@Log
public class WUDInfoParser {
public final class WUDInfoParser {
public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
public final static int PARTITION_TOC_OFFSET = 0x800;
@ -28,11 +27,12 @@ public class WUDInfoParser {
public static final String WUD_TICKET_FILENAME = "title.tik";
public static final String WUD_CERT_FILENAME = "title.cert";
public static WUDInfo createAndLoad(WUDDiscReader discReader,byte[] titleKey) throws IOException {
WUDInfo result = new WUDInfo();
private WUDInfoParser(){
//
}
result.setTitleKey(titleKey);
result.setWUDDiscReader(discReader);
public static WUDInfo createAndLoad(WUDDiscReader discReader,byte[] titleKey) throws IOException {
WUDInfo result = new WUDInfo(titleKey,discReader);
byte[] PartitionTocBlock = discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET, 0, 0x8000, titleKey, null);
@ -43,7 +43,7 @@ public class WUDInfoParser {
}
Map<String,WUDPartition> partitions = readPartitions(result,PartitionTocBlock);
result.setPartitions(partitions);
result.getPartitions().putAll(partitions);
//parsePartitions(wudInfo,partitions);
return result;
@ -61,12 +61,14 @@ public class WUDInfoParser {
Map<String,WUDPartition> partitions = new HashMap<>();
WUDGamePartition gamePartition = new WUDGamePartition();
byte[] gamePartitionTMD = new byte[0];
byte[] gamePartitionTicket = new byte[0];
byte[] gamePartitionCert = new byte[0];
String realGamePartitionName = null;
// populate partition information from decrypted TOC
for (int i = 0; i < partitionCount; i++){
WUDPartition partition = new WUDPartition();
int offset = (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE));
byte[] partitionIdentifier = Arrays.copyOfRange(partitionTocBlock, offset, offset+ 0x19);
@ -83,9 +85,7 @@ public class WUDInfoParser {
long partitionOffset = ((tmp * (long)0x8000) - 0x10000);
partition.setPartitionName(partitionName);
partition.setPartitionOffset(partitionOffset);
WUDPartition partition = new WUDPartition(partitionName,partitionOffset);
if(partitionName.startsWith("SI")){
byte[] fileTableBlock = wudInfo.getWUDDiscReader().readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + partitionOffset,0, 0x8000, wudInfo.getTitleKey(),null);
@ -100,18 +100,14 @@ public class WUDInfoParser {
byte[] rawTMD = getFSTEntryAsByte(WUD_TMD_FILENAME,partition,fst,wudInfo.getWUDDiscReader(),wudInfo.getTitleKey());
byte[] rawCert = getFSTEntryAsByte(WUD_CERT_FILENAME,partition,fst,wudInfo.getWUDDiscReader(),wudInfo.getTitleKey());
gamePartition.setRawTMD(rawTMD);
gamePartition.setRawTicket(rawTIK);
gamePartition.setRawCert(rawCert);
gamePartitionTMD = rawTMD;
gamePartitionTicket = rawTIK;
gamePartitionCert = rawCert;
//We want to use the real game partition
realGamePartitionName = partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08));
}else if(partitionName.startsWith(realGamePartitionName)){
gamePartition.setPartitionOffset(partitionOffset);
gamePartition.setPartitionName(partitionName);
wudInfo.setGamePartitionName(partitionName);
partition = gamePartition;
partition = new WUDGamePartition(partitionName, partitionOffset, gamePartitionTMD, gamePartitionCert, gamePartitionTicket);
}
byte [] header = wudInfo.getWUDDiscReader().readEncryptedToByteArray(partition.getPartitionOffset()+0x10000,0,0x8000);
WUDPartitionHeader partitionHeader = WUDPartitionHeader.parseHeader(header);

View File

@ -3,8 +3,8 @@ package de.mas.jnus.lib.implementations.wud.parser;
import lombok.Data;
@Data
public class WUDPartition {
private String partitionName = "";
private long partitionOffset = 0;
private final String partitionName;
private final long partitionOffset;
private WUDPartitionHeader partitionHeader;
}

View File

@ -17,10 +17,11 @@ import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class WUDPartitionHeader {
public final class WUDPartitionHeader {
@Getter @Setter
private boolean calculatedHashes = false;
private HashMap<Short,byte[]> h3Hashes;
@Getter
private final HashMap<Short,byte[]> h3Hashes = new HashMap<>();
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
private byte[] rawData;
@ -34,13 +35,6 @@ public class WUDPartitionHeader {
return result;
}
public HashMap<Short,byte[]> getH3Hashes() {
if(h3Hashes == null){
h3Hashes = new HashMap<>();
}
return h3Hashes;
}
public void addH3Hashes(short index, byte[] hash) {
getH3Hashes().put(index, hash);
}

View File

@ -13,14 +13,13 @@ import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public abstract class WUDDiscReader {
@Getter @Setter private WUDImage image = null;
@Getter private final WUDImage image;
public WUDDiscReader(WUDImage image){
setImage(image);
this.image = image;
}
public InputStream readEncryptedToInputStream(long offset,long size) throws IOException {
@ -81,10 +80,12 @@ public abstract class WUDDiscReader {
}
public void readDecryptedToOutputStream(OutputStream outputStream,long clusterOffset, long fileOffset, long size,byte[] key,byte[] IV) throws IOException {
if(IV == null){
IV = new byte[0x10];
byte[] usedIV = IV;
if(usedIV == null){
usedIV = new byte[0x10];
}
long usedSize = size;
long usedFileOffset = fileOffset;
byte[] buffer;
long maxCopySize;
@ -96,23 +97,23 @@ public abstract class WUDDiscReader {
long totalread = 0;
do{
long blockNumber = (fileOffset / blockSize);
long blockOffset = (fileOffset % blockSize);
long blockNumber = (usedFileOffset / blockSize);
long blockOffset = (usedFileOffset % blockSize);
readOffset = clusterOffset + (blockNumber * blockSize);
// (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000);
buffer = readDecryptedChunk(readOffset,key, IV);
buffer = readDecryptedChunk(readOffset,key, usedIV);
maxCopySize = 0x8000 - blockOffset;
copySize = (size > maxCopySize) ? maxCopySize : size;
copySize = (usedSize > maxCopySize) ? maxCopySize : usedSize;
outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize));
totalread += copySize;
// update counters
size -= copySize;
fileOffset += copySize;
}while(totalread < size);
usedSize -= copySize;
usedFileOffset += copySize;
}while(totalread < usedSize);
outputStream.close();
}

View File

@ -25,12 +25,16 @@ public class WUDDiscReaderCompressed extends WUDDiscReader{
WUDImageCompressedInfo info = getImage().getCompressedInfo();
long fileBytesLeft = info.getUncompressedSize() - offset;
long usedOffset = offset;
long usedSize = size;
if( fileBytesLeft <= 0 ){
log.warning("offset too big");
System.exit(1);
}
if( fileBytesLeft < size ){
size = fileBytesLeft;
if( fileBytesLeft < usedSize ){
usedSize = fileBytesLeft;
}
// compressed read must be handled on a per-sector level
@ -38,11 +42,11 @@ public class WUDDiscReaderCompressed extends WUDDiscReader{
byte[] buffer = new byte[bufferSize];
RandomAccessFile input = getRandomAccessFileStream();
while( size > 0 ){
long sectorOffset = (offset % info.getSectorSize());
while( usedSize > 0 ){
long sectorOffset = (usedOffset % info.getSectorSize());
long remainingSectorBytes = info.getSectorSize() - sectorOffset;
long sectorIndex = (offset / info.getSectorSize());
int bytesToRead = (int) ((remainingSectorBytes<size)?remainingSectorBytes:size); // read only up to the end of the current sector
long sectorIndex = (usedOffset / info.getSectorSize());
int bytesToRead = (int) ((remainingSectorBytes<usedSize)?remainingSectorBytes:usedSize); // read only up to the end of the current sector
// look up real sector index
long realSectorIndex = info.getSectorIndex((int) sectorIndex);
long offset2 = info.getOffsetSectorArray() + realSectorIndex*info.getSectorSize()+sectorOffset;
@ -60,10 +64,9 @@ public class WUDDiscReaderCompressed extends WUDDiscReader{
}
}
size -= bytesToRead;
offset += bytesToRead;
usedSize -= bytesToRead;
usedOffset += bytesToRead;
}
input.close();
}
}

View File

@ -9,13 +9,14 @@ import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
public class WUDDiscReaderSplitted extends WUDDiscReader{
public WUDDiscReaderSplitted(WUDImage image) {
super(image);
}
public static long WUD_SPLITTED_FILE_SIZE = 0x100000L * 0x800L;
public static long NUMBER_OF_FILES = 12;
public static String WUD_SPLITTED_DEFAULT_FILEPATTERN = "game_part%d.wud";
public WUDDiscReaderSplitted(WUDImage image) {
super(image);
}
@Override
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset, long size) throws IOException {
RandomAccessFile input = getFileByOffset(offset);

View File

@ -6,6 +6,7 @@ import java.io.OutputStream;
import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.utils.StreamUtils;
public class WUDDiscReaderUncompressed extends WUDDiscReader {
public WUDDiscReaderUncompressed(WUDImage image) {
@ -16,7 +17,9 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset,long size) throws IOException{
FileInputStream input = new FileInputStream(getImage().getFileHandle());
input.skip(offset);
StreamUtils.skipExactly(input,offset);
int bufferSize = 0x8000;
byte[] buffer = new byte[bufferSize];
long totalread = 0;

View File

@ -4,7 +4,12 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public class ByteUtils {
public final class ByteUtils {
private ByteUtils(){
//Utility Class
}
public static int getIntFromBytes(byte[] input,int offset){
return getIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
}

View File

@ -7,8 +7,10 @@ import java.io.InputStream;
import lombok.NonNull;
public class FileUtils {
public final class FileUtils {
private FileUtils(){
//Utility Class
}
public static boolean saveByteArrayToFile(String filePath,byte[] data) throws IOException {
File target = new File(filePath);
if(target.isDirectory()){

View File

@ -12,7 +12,10 @@ import java.util.Arrays;
import lombok.extern.java.Log;
@Log
public class HashUtil {
public final class HashUtil {
private HashUtil(){
//Utility class
}
public static byte[] hashSHA256(byte[] data){
MessageDigest sha256;
try {

View File

@ -1,5 +1,6 @@
package de.mas.jnus.lib.utils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -10,7 +11,10 @@ import java.util.Arrays;
import lombok.extern.java.Log;
@Log
public class StreamUtils {
public final class StreamUtils {
private StreamUtils(){
//Utility class
}
public static byte[] getBytesFromStream(InputStream in,int size) throws IOException{
byte[] result = new byte[size];
byte[] buffer = new byte[0x8000];
@ -124,4 +128,16 @@ public class StreamUtils {
outputStream.close();
inputStream.close();
}
public static void skipExactly(InputStream in, long offset) throws IOException {
long n = offset;
while (n != 0) {
long skipped = in.skip(n);
if (skipped == 0){
in.close();
throw new EOFException();
}
n -= skipped;
}
}
}

View File

@ -2,8 +2,10 @@ package de.mas.jnus.lib.utils;
import java.io.File;
public class Utils {
public final class Utils {
private Utils(){
//Utility class
}
public static long align(long numToRound, int multiple){
if((multiple>0) && ((multiple & (multiple -1)) == 0)){
return alignPower2(numToRound, multiple);

View File

@ -31,7 +31,7 @@ public class AESDecryption {
init();
}
protected void init() {
protected final void init() {
init(getAESKey(),getIV());
}

View File

@ -96,7 +96,7 @@ public class NUSDecryption extends AESDecryption{
log.info(Utils.ByteArrayToString(expected_hash));
log.info("Hash doesn't match decrypted content.");
}else{
//log.warning("###################################################Hash DOES match saves output stream.");
//log.warning("Hash DOES match saves output stream.");
}
}

View File

@ -5,7 +5,10 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class Downloader {
public abstract class Downloader {
public Downloader() {
//
}
public static byte[] downloadFileToByteArray(String fileURL) throws IOException {
int BUFFER_SIZE = 0x800;
URL url = new URL(fileURL);

View File

@ -4,34 +4,38 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.Settings;
import lombok.Setter;
public class NUSDownloadService extends Downloader{
public final class NUSDownloadService extends Downloader{
private static NUSDownloadService defaultInstance;
private static Map<String,NUSDownloadService> instances = new HashMap<>();
private final String URL_BASE;
private NUSDownloadService(String URL){
this.URL_BASE = URL;
}
public static NUSDownloadService getDefaultInstance(){
synchronized (defaultInstance) {
if(defaultInstance == null){
defaultInstance = new NUSDownloadService();
defaultInstance.setURL_BASE(Settings.URL_BASE);
defaultInstance = new NUSDownloadService(Settings.URL_BASE);
}
}
return defaultInstance;
}
public static NUSDownloadService getInstance(String URL){
NUSDownloadService instance = new NUSDownloadService();
instance.setURL_BASE(URL);
return instance;
if(!instances.containsKey(URL)){
NUSDownloadService instance = new NUSDownloadService(URL);
instances.put(URL, instance);
}
private NUSDownloadService(){
return instances.get(URL);
}
@Setter private String URL_BASE = "";
public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
String version_suf = "";
if(version > Settings.LATEST_TMD_VERSION) version_suf = "." + version;