From d999e2fedb6948b838bb244a9ad431e2d4fd960d Mon Sep 17 00:00:00 2001 From: BullyWiiPlaza Date: Thu, 4 Feb 2016 01:18:52 +0100 Subject: [PATCH] Refactor code --- .classpath | 6 - .gitattributes | 17 - .project | 17 - .settings/org.eclipse.core.resources.prefs | 2 - .settings/org.eclipse.jdt.core.prefs | 11 - jar/JNUSTool.jar | Bin 36011 -> 0 bytes jar/JNUSTool_Java7.jar | Bin 36003 -> 0 bytes jar/config | 2 - src/de/mas/jnustool/Content.java | 28 +- src/de/mas/jnustool/ContentInfo.java | 42 +- src/de/mas/jnustool/Directory.java | 108 ++-- src/de/mas/jnustool/FEntry.java | 266 ++++---- src/de/mas/jnustool/FEntryDownloader.java | 31 +- src/de/mas/jnustool/FST.java | 392 ++++++------ src/de/mas/jnustool/NUSTitle.java | 259 ++++---- src/de/mas/jnustool/Starter.java | 54 -- src/de/mas/jnustool/TIK.java | 106 ++-- src/de/mas/jnustool/TitleMetaData.java | 261 ++++---- src/de/mas/jnustool/gui/JCheckBoxTree.java | 479 +++++++------- src/de/mas/jnustool/gui/NUSGUI.java | 80 --- src/de/mas/jnustool/util/Decryption.java | 659 +++++++++++--------- src/de/mas/jnustool/util/Downloader.java | 249 ++++---- src/de/mas/jnustool/util/ExitException.java | 15 +- src/de/mas/jnustool/util/Settings.java | 7 +- src/de/mas/jnustool/util/Util.java | 62 -- 25 files changed, 1528 insertions(+), 1625 deletions(-) delete mode 100644 .classpath delete mode 100644 .gitattributes delete mode 100644 .project delete mode 100644 .settings/org.eclipse.core.resources.prefs delete mode 100644 .settings/org.eclipse.jdt.core.prefs delete mode 100644 jar/JNUSTool.jar delete mode 100644 jar/JNUSTool_Java7.jar delete mode 100644 jar/config delete mode 100644 src/de/mas/jnustool/Starter.java delete mode 100644 src/de/mas/jnustool/gui/NUSGUI.java delete mode 100644 src/de/mas/jnustool/util/Util.java diff --git a/.classpath b/.classpath deleted file mode 100644 index fceb480..0000000 --- a/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index bdb0cab..0000000 --- a/.gitattributes +++ /dev/null @@ -1,17 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto - -# Custom for Visual Studio -*.cs diff=csharp - -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain diff --git a/.project b/.project deleted file mode 100644 index 51d1814..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - JNusTool - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 82bae0f..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/Util.java=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 3a21537..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,11 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 diff --git a/jar/JNUSTool.jar b/jar/JNUSTool.jar deleted file mode 100644 index 2aeef33120ecc0a785c0df68f748f0a77da59cb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36011 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

(*bJTv#-FUfr2gd|M*v8CV#{CDz^dzc||DefYsF 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 XjhX;&R$!USz#svHazIvo8;A!0t$}_P diff --git a/jar/config b/jar/config deleted file mode 100644 index 3814033..0000000 --- a/jar/config +++ /dev/null @@ -1,2 +0,0 @@ -http://ccs.cdn.wup.shop.nintendo.net/ccs/download -[COMMONKEY] \ No newline at end of file diff --git a/src/de/mas/jnustool/Content.java b/src/de/mas/jnustool/Content.java index 8fd28ca..741130a 100644 --- a/src/de/mas/jnustool/Content.java +++ b/src/de/mas/jnustool/Content.java @@ -1,25 +1,27 @@ package de.mas.jnustool; -import de.mas.jnustool.util.Util; +import de.mas.jnustool.util.ConversionUtils; -public class Content { - - int ID; // 0 0xB04 - short index; // 4 0xB08 - short type; // 6 0xB0A - long size; // 8 0xB0C - byte[] SHA2 = new byte[32]; // 16 0xB14 - - - public Content(int ID, short index, short type, long size, byte[] SHA2) { +public class Content +{ + int ID; // 0 0xB04 + short index; // 4 0xB08 + short type; // 6 0xB0A + long size; // 8 0xB0C + byte[] SHA2 = new byte[32]; // 16 0xB14 + + public Content(int ID, short index, short type, long size, byte[] SHA2) + { this.ID = ID; this.index = index; this.type = type; this.size = size; this.SHA2 = SHA2; } + @Override - public String toString(){ - return "ID: " + ID +" index: " + index + " type: " + type + " size: " + size + " SHA2: " + Util.ByteArrayToString(SHA2); + public String toString() + { + return "ID: " + ID + " index: " + index + " type: " + type + " size: " + size + " SHA2: " + ConversionUtils.ByteArrayToString(SHA2); } } diff --git a/src/de/mas/jnustool/ContentInfo.java b/src/de/mas/jnustool/ContentInfo.java index add9032..4b6c19c 100644 --- a/src/de/mas/jnustool/ContentInfo.java +++ b/src/de/mas/jnustool/ContentInfo.java @@ -1,32 +1,34 @@ package de.mas.jnustool; -import de.mas.jnustool.util.Util; +import de.mas.jnustool.util.ConversionUtils; -public class ContentInfo { - public short indexOffset; // 0 0x204 - public short commandCount; // 2 0x206 - public byte[] SHA2 = new byte[32]; // 12 0x208 - - //TODO: Test, size checking - /* - * untested - */ - - public ContentInfo(byte[] info){ - this.indexOffset=(short)( ((info[0]&0xFF)<<8) | (info[1]&0xFF) ); - this.commandCount=(short)( ((info[2]&0xFF)<<8) | (info[3]&0xFF) ); - for(int i = 0;i<32;i++){ - this.SHA2[i] = info[4+i]; +public class ContentInfo +{ + public short indexOffset; // 0 0x204 + public short commandCount; // 2 0x206 + public byte[] SHA2 = new byte[32]; // 12 0x208 + + // TODO: Test, size checking + public ContentInfo(byte[] info) + { + this.indexOffset = (short) (((info[0] & 0xFF) << 8) | (info[1] & 0xFF)); + this.commandCount = (short) (((info[2] & 0xFF) << 8) | (info[3] & 0xFF)); + for (int i = 0; i < 32; i++) + { + this.SHA2[i] = info[4 + i]; } } - public ContentInfo(short indexOffset, short commandCount, byte[] SHA2) { + public ContentInfo(short indexOffset, short commandCount, byte[] SHA2) + { this.indexOffset = indexOffset; this.commandCount = commandCount; this.SHA2 = SHA2; } + @Override - public String toString(){ - return "indexOffset: " + indexOffset +" commandCount: " + commandCount + " SHA2: " + Util.ByteArrayToString(SHA2); + public String toString() + { + return "indexOffset: " + indexOffset + " commandCount: " + commandCount + " SHA2: " + ConversionUtils.ByteArrayToString(SHA2); } -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/Directory.java b/src/de/mas/jnustool/Directory.java index 75bc4e1..cc8f4d5 100644 --- a/src/de/mas/jnustool/Directory.java +++ b/src/de/mas/jnustool/Directory.java @@ -5,89 +5,85 @@ import java.util.TreeMap; import javax.swing.tree.DefaultMutableTreeNode; -public class Directory { +public class Directory +{ String name = ""; - TreeMap folder = new TreeMap<>(); - TreeMap files = new TreeMap<>(); - - public Directory get(String s){ + TreeMap folder = new TreeMap<>(); + TreeMap files = new TreeMap<>(); + + public Directory get(String s) + { return folder.get(s); } - - public Directory(String name){ + + public Directory(String name) + { setName(name); } - public boolean containsFolder(String s){ + public boolean containsFolder(String s) + { return folder.containsKey(s); } - - public Directory getFolder(String s){ + + public Directory getFolder(String s) + { return folder.get(s); } - - public Directory addFolder(Directory s){ - return folder.put(s.getName(),s); + + public Directory addFolder(Directory s) + { + return folder.put(s.getName(), s); } - public boolean containsFile(String s){ - return files.containsKey(s); + + public FEntry addFile(FEntry s) + { + return files.put(s.getFileName(), s); } - - public FEntry getFile(String s){ - return files.get(s); - } - - public FEntry addFile(FEntry s){ - return files.put(s.getFileName(),s); - } - - public String getName() { + + public String getName() + { return name; } - public void setName(String name) { + public void setName(String name) + { this.name = name; } - - - public Collection getFolder() { - return folder.values(); + + public Collection getFolder() + { + return folder.values(); } - - - public Collection getFiles() { - return files.values(); - } - - public void setFiles(TreeMap files) { - this.files = files; + public Collection getFiles() + { + return files.values(); } @Override - public String toString(){ + public String toString() + { System.out.println(name + ":"); - for(Directory d : folder.values()){ - System.out.println(d); - } - for(String s : files.keySet()){ - System.out.println(s); - } + folder.values().forEach(System.out::println); + files.keySet().forEach(System.out::println); + return ""; } - - public DefaultMutableTreeNode getNodes(){ + + public DefaultMutableTreeNode getNodes() + { DefaultMutableTreeNode node = new DefaultMutableTreeNode(getName()); - - for(Directory f: getFolder()){ - node.add(f.getNodes()); - } - - for(FEntry f: getFiles()){ + + for (Directory directory : getFolder()) + { + node.add(directory.getNodes()); + } + + for (FEntry f : getFiles()) + { node.add(new DefaultMutableTreeNode(f)); } return node; } - - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/FEntry.java b/src/de/mas/jnustool/FEntry.java index b19b9f5..57b8815 100644 --- a/src/de/mas/jnustool/FEntry.java +++ b/src/de/mas/jnustool/FEntry.java @@ -2,6 +2,7 @@ package de.mas.jnustool; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import de.mas.jnustool.util.Decryption; @@ -9,29 +10,30 @@ import de.mas.jnustool.util.Downloader; import de.mas.jnustool.util.ExitException; import de.mas.jnustool.util.Settings; -public class FEntry { +public class FEntry +{ private FST fst; - + public static int DIR_FLAG = 1; - public static int NOT_IN_NUSTITLE_FLAG = 0x80; - public static int EXTRACT_WITH_HASH_FLAG = 0x440; + public static int NOT_IN_NUS_TITLE_FLAG = 0x80; + public static int EXTRACT_WITH_HASH_FLAG = 0x440; public static int CHANGE_OFFSET_FLAG = 0x04; - + private boolean dir = false; private boolean in_nus_title = false; private boolean extract_withHash = false; - + private String fileName = ""; private String path = ""; - private long fileOffset = 0L; + private long fileOffset = 0L; private long fileLength = 0; private int contentID = 0; - private int NUScontentID = 0; + private int NUSContentID = 0; private List pathList; - - public FEntry(String path, String filename, int contentID,int NUScontentID, long fileOffset, long fileLength, boolean dir, - boolean in_nus_title, boolean extract_withHash, List pathList,FST fst) { + public FEntry(String path, String filename, int contentID, int NUSContentID, long fileOffset, long fileLength, boolean dir, + boolean in_nus_title, boolean extract_withHash, List pathList, FST fst) + { setPath(path); setFileName(filename); setContentID(contentID); @@ -40,202 +42,246 @@ public class FEntry { setDir(dir); setInNusTitle(in_nus_title); setExtractWithHash(extract_withHash); - setNUScontentID(NUScontentID); + setNUSContentID(NUSContentID); setPathList(pathList); this.fst = fst; } - public boolean isDir() { + public boolean isDir() + { return dir; } - private void setDir(boolean dir) { + private void setDir(boolean dir) + { this.dir = dir; } - public boolean isInNUSTitle() { + public boolean isInNUSTitle() + { return in_nus_title; } - private void setInNusTitle(boolean in_nus_title) { + private void setInNusTitle(boolean in_nus_title) + { this.in_nus_title = in_nus_title; } - public boolean isExtractWithHash() { + public boolean isExtractWithHash() + { return extract_withHash; } - private void setExtractWithHash(boolean extract_withHash) { + private void setExtractWithHash(boolean extract_withHash) + { this.extract_withHash = extract_withHash; } - public String getFileName() { + public String getFileName() + { return fileName; } - private void setFileName(String filename) { + private void setFileName(String filename) + { this.fileName = filename; } - public String getPath() { - return path; - } - - public String getFullPath() { + public String getFullPath() + { return path + fileName; } - private void setPath(String path) { + private void setPath(String path) + { this.path = path; } - public long getFileOffset() { + public long getFileOffset() + { return fileOffset; } - private void setFileOffset(long fileOffset) { + private void setFileOffset(long fileOffset) + { this.fileOffset = fileOffset; } - public int getContentID() { + public int getContentID() + { return contentID; } - private void setContentID(int contentID) { + private void setContentID(int contentID) + { this.contentID = contentID; } - public long getFileLength() { + public long getFileLength() + { return fileLength; } - private void setFileLength(long fileLength) { + private void setFileLength(long fileLength) + { this.fileLength = fileLength; } - + @Override - public String toString(){ - return getFullPath() + " Content ID:" + contentID + " Size: " + fileLength +"MB Offset: " + fileOffset; + public String toString() + { + return getFullPath() + " Content ID:" + contentID + " Size: " + fileLength + "MB Offset: " + fileOffset; } - public int getNUScontentID() { - return NUScontentID; + public int getNUSContentID() + { + return NUSContentID; } - private void setNUScontentID(int nUScontentID) { - NUScontentID = nUScontentID; + private void setNUSContentID(int nusContentID) + { + NUSContentID = nusContentID; } - - private void createFolder() { + + private void createFolder() + { long titleID = getTitleID(); - String [] path = getFullPath().split("/"); - File f = new File (String.format("%016X", titleID)); - if(!f.exists())f.mkdir(); - - String folder = String.format("%016X", titleID) +"/"; - File folder_ = null; - for(int i = 0;i getPathList() { + public List getPathList() + { return pathList; } - public void setPathList(List pathList) { + public void setPathList(List pathList) + { this.pathList = pathList; } - public String getContentPath() { - return fst.getTmd().getContentPath() + "/" + String.format("%08X", getNUScontentID()) + ".app"; + public String getContentPath() + { + return fst.getTmd().getContentPath() + "/" + String.format("%08X", getNUSContentID()) + ".app"; } - public long getTitleID() { + public long getTitleID() + { return fst.getTmd().titleID; } - public TIK getTicket() { + public TIK getTicket() + { return fst.getTmd().getNUSTitle().getTicket(); } - - - - - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/FEntryDownloader.java b/src/de/mas/jnustool/FEntryDownloader.java index 755927b..886f8c9 100644 --- a/src/de/mas/jnustool/FEntryDownloader.java +++ b/src/de/mas/jnustool/FEntryDownloader.java @@ -4,19 +4,22 @@ import java.util.concurrent.Callable; public class FEntryDownloader implements Callable { - FEntry f; - public void setTitle(FEntry f){ - this.f = f; - } - public FEntryDownloader(FEntry f){ - setTitle(f); - } - - - @Override - public Integer call() throws Exception { - f.downloadAndDecrypt(); - return null; + private FEntry fEntry; + + public void setTitle(FEntry fEntry) + { + this.fEntry = fEntry; } -} + public FEntryDownloader(FEntry fEntry) + { + setTitle(fEntry); + } + + @Override + public Integer call() throws Exception + { + fEntry.downloadAndDecrypt(); + return null; + } +} \ No newline at end of file diff --git a/src/de/mas/jnustool/FST.java b/src/de/mas/jnustool/FST.java index b4887cf..a555ff9 100644 --- a/src/de/mas/jnustool/FST.java +++ b/src/de/mas/jnustool/FST.java @@ -5,310 +5,288 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import de.mas.jnustool.util.Util; +import de.mas.jnustool.util.ConversionUtils; -public class FST { +public class FST +{ private TitleMetaData tmd; long totalContentSize = 0L; long totalContentSizeInNUS = 0L; - + List fileEntries = new ArrayList<>(); - + int totalContentCount = 0; - + int totalEntries = 0; int dirEntries = 0; - + private Directory FSTDirectory = new Directory("root"); - + private Directory contentDirectory = new Directory("root"); - - public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException { - parse(decrypteddata,tmd); + + public FST(byte[] decryptedData, TitleMetaData tmd) throws IOException + { + parse(decryptedData, tmd); setTmd(tmd); buildDirectories(); - } + } - private void buildDirectories() { - - String contentfolder = ""; - Directory 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); - contentDirectory.addFolder(newDir); + private void buildDirectories() + { + String contentFolder; + Directory curContent; + 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); + contentDirectory.addFolder(newDir); } - curContent = contentDirectory.getFolder(contentfolder); - - Directory current = FSTDirectory; - int i = 0; - - for(String s :f.getPathList()){ - i++; - + curContent = contentDirectory.getFolder(contentFolder); + + Directory current = FSTDirectory; + int i = 0; + + for (String s : f.getPathList()) + { + i++; + //Content - if(curContent.containsFolder(s)){ + if (curContent.containsFolder(s)) + { curContent = curContent.get(s); - }else{ + } else + { Directory newDir = new Directory(s); curContent.addFolder(newDir); curContent = newDir; - } - if(i==f.getPathList().size()){ + } + if (i == f.getPathList().size()) + { curContent.addFile(f); } - - + //FST - if(current.containsFolder(s)){ + if (current.containsFolder(s)) + { current = current.get(s); - }else{ + } else + { Directory newDir = new Directory(s); current.addFolder(newDir); current = newDir; } - if(i==f.getPathList().size()){ + if (i == f.getPathList().size()) + { current.addFile(f); } - } - } - } - - - + } + } + } } - - - private void parse(byte[] decrypteddata, TitleMetaData tmd) throws IOException { - - if(!Arrays.equals(Arrays.copyOfRange(decrypteddata, 0, 3), new byte[]{0x46,0x53,0x54})){ + private void parse(byte[] decryptedData, TitleMetaData tmd) throws IOException + { + if (!Arrays.equals(Arrays.copyOfRange(decryptedData, 0, 3), new byte[]{0x46, 0x53, 0x54})) + { 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); + + this.totalContentCount = ConversionUtils.getIntFromBytes(decryptedData, 8); + int base_offset = 0x20 + totalContentCount * 0x20; + + this.totalEntries = ConversionUtils.getIntFromBytes(decryptedData, base_offset + 8); int nameOff = base_offset + totalEntries * 0x10; - - int level=0; + + int level = 0; int[] LEntry = new int[16]; int[] Entry = new int[16]; - - for(int i = 0;i 0) + + if (level > 0) { - while( LEntry[level-1] == i ) - { + while (LEntry[level - 1] == i) + { level--; } } - - - int offset = base_offset + i*0x10; - + + int offset = base_offset + i * 0x10; + //getting the type - type = (int) decrypteddata[offset]+128; - if((type & FEntry.DIR_FLAG) == 1) dir = true; - if((type & FEntry.NOT_IN_NUSTITLE_FLAG) == 0 ) in_nus_title = false; - - + type = (int) decryptedData[offset] + 128; + if ((type & FEntry.DIR_FLAG) == 1) + { + dir = true; + } + if ((type & FEntry.NOT_IN_NUS_TITLE_FLAG) == 0) + { + in_nus_title = false; + } + //getting Name - decrypteddata[offset] = 0; - int nameoff_entry_offset = Util.getIntFromBytes(decrypteddata, offset); + decryptedData[offset] = 0; + int nameOff_entry_offset = ConversionUtils.getIntFromBytes(decryptedData, offset); int j = 0; - int nameoff_entry = nameOff + nameoff_entry_offset; - while(decrypteddata[nameoff_entry + j] != 0){j++;} - filename = new String(Arrays.copyOfRange(decrypteddata,nameoff_entry, nameoff_entry + j)); - + int nameOff_entry = nameOff + nameOff_entry_offset; + while (decryptedData[nameOff_entry + j] != 0) + { + j++; + } + filename = new String(Arrays.copyOfRange(decryptedData, nameOff_entry, nameOff_entry + j)); + //getting offsets. save in two ways - offset+=4; - fileOffset = (long) Util.getIntFromBytes(decrypteddata, offset); - offset+=4; - fileLength = Util.getIntAsLongFromBytes(decrypteddata, offset); - @SuppressWarnings("unused") - int parentOffset = (int) fileOffset; + offset += 4; + fileOffset = (long) ConversionUtils.getIntFromBytes(decryptedData, offset); + offset += 4; + fileLength = ConversionUtils.getIntAsLongFromBytes(decryptedData, offset); int nextOffset = (int) fileLength; - - + //grabbing flags - offset+=4; - int flags = Util.getShortFromBytes(decrypteddata, offset); - if((flags & FEntry.EXTRACT_WITH_HASH_FLAG) > 0) extract_withHash = true; - if((flags & FEntry.CHANGE_OFFSET_FLAG) == 0) fileOffset <<=5; - - //grabbing contentid - offset+=2; - contentID = Util.getShortFromBytes(decrypteddata, offset) ; - - + offset += 4; + int flags = ConversionUtils.getShortFromBytes(decryptedData, offset); + if ((flags & FEntry.EXTRACT_WITH_HASH_FLAG) > 0) + { + extract_withHash = true; + } + if ((flags & FEntry.CHANGE_OFFSET_FLAG) == 0) + { + fileOffset <<= 5; + } + + //grabbing content id + offset += 2; + contentID = ConversionUtils.getShortFromBytes(decryptedData, offset); + //remember total size this.totalContentSize += fileLength; - if(in_nus_title)this.totalContentSizeInNUS += fileLength; - + if (in_nus_title) + { + this.totalContentSizeInNUS += fileLength; + } + List pathList = new ArrayList<>(); //getting the full path of entry - if(dir) + if (dir) { dirEntries++; Entry[level] = i; - LEntry[level++] = nextOffset ; - if( level > 15 ) // something is wrong! + LEntry[level++] = nextOffset; + if (level > 15) // something is wrong! { break; } - }else{ - StringBuilder sb = new StringBuilder(); - int k = 0; - int nameoffoff,nameoff_entrypath; + } else + { + StringBuilder stringBuilder = new StringBuilder(); + int k; + int nameOffOff, nameOff_entryPath; - for( j=0; j getFileEntries() { + public List getFileEntries() + { return fileEntries; } - - public void setFileEntries(List fileEntries) { - this.fileEntries = fileEntries; - } - - - public int getTotalContentCount() { - return totalContentCount; - } - - - public void setTotalContentCount(int totalContentCount) { - this.totalContentCount = totalContentCount; - } - - - public int getTotalEntries() { + public int getTotalEntries() + { return totalEntries; } - - public void setTotalEntries(int totalEntries) { - this.totalEntries = totalEntries; - } - - - public int getDirEntries() { - return dirEntries; - } - - - public void setDirEntries(int dirEntries) { - this.dirEntries = dirEntries; - } - - @Override - public String toString(){ - return "entryCount: " + totalContentCount+ " entries: " + totalEntries; + public String toString() + { + return "entryCount: " + totalContentCount + " entries: " + totalEntries; } - - public int getFileCount() { + public int getFileCount() + { int i = 0; - for(FEntry f: getFileEntries()){ - if(!f.isDir()) + for (FEntry f : getFileEntries()) + { + if (!f.isDir()) + { i++; - } - return i; - } - - public int getFileCountInNUS() { - int i = 0; - for(FEntry f: getFileEntries()){ - if(!f.isDir() && f.isInNUSTitle()) - i++; - } + } + } return i; } - public Directory getFSTDirectory() { + public int getFileCountInNUS() + { + int i = 0; + for (FEntry f : getFileEntries()) + { + if (!f.isDir() && f.isInNUSTitle()) + { + i++; + } + } + return i; + } + + public Directory getFSTDirectory() + { return FSTDirectory; } - - public Directory getContentDirectory() { - return contentDirectory; - } - - - public TitleMetaData getTmd() { + public TitleMetaData getTmd() + { return tmd; } - public void setTmd(TitleMetaData tmd) { + public void setTmd(TitleMetaData tmd) + { this.tmd = tmd; } - - - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/NUSTitle.java b/src/de/mas/jnustool/NUSTitle.java index a4b97e0..1c443af 100644 --- a/src/de/mas/jnustool/NUSTitle.java +++ b/src/de/mas/jnustool/NUSTitle.java @@ -11,203 +11,230 @@ import de.mas.jnustool.util.Downloader; import de.mas.jnustool.util.ExitException; import de.mas.jnustool.util.Settings; -public class NUSTitle { +public class NUSTitle +{ private TitleMetaData tmd; private TIK ticket; private FST fst; private long titleID; - public NUSTitle(long titleId,String key) throws ExitException{ + + public NUSTitle(long titleId, String key) throws ExitException + { setTitleID(titleId); - try { - if(Settings.downloadContent){ - File f = new File(getContentPath()); - if(!f.exists())f.mkdir(); + try + { + if (Settings.downloadContent) + { + File f = new File(getContentPath()); + if (!f.exists()) + { + Files.createDirectory(f.toPath()); + } } - if(Settings.downloadContent){ - + if (Settings.downloadContent) + { + File f = new File(getContentPath() + "/" + "tmd"); - if(!(f.exists() && Settings.skipExistingTMDTICKET)){ + if (!(f.exists() && Settings.skipExistingTMDTICKET)) + { System.out.println("Downloading TMD"); - Downloader.getInstance().downloadTMD(titleId,getContentPath()); - }else{ + Downloader.getInstance().downloadTMD(titleId, getContentPath()); + } else + { System.out.println("Skipped download of TMD. Already existing"); } f = new File(getContentPath() + "/" + "cetk"); - if(!(f.exists() && Settings.skipExistingTMDTICKET)){ - if(key == null){ + if (!(f.exists() && Settings.skipExistingTMDTICKET)) + { + if (key == null) + { System.out.print("Downloading Ticket"); - Downloader.getInstance().downloadTicket(titleId,getContentPath()); + Downloader.getInstance().downloadTicket(titleId, getContentPath()); } - }else{ + } else + { System.out.println("Skipped download of ticket. Already existing"); } } - - if(Settings.useCachedFiles){ + + if (Settings.useCachedFiles) + { File f = new File(getContentPath() + "/" + "tmd"); - if(f.exists()){ + if (f.exists()) + { System.out.println("Using cached TMD."); tmd = new TitleMetaData(f); - }else{ + } else + { System.out.println("No cached TMD found."); } } - - if(tmd == null){ - if(Settings.downloadWhenCachedFilesMissingOrBroken){ - if(Settings.useCachedFiles) System.out.println("Getting missing tmd from Server!"); + + if (tmd == null) + { + if (Settings.downloadWhenCachedFilesMissingOrBroken) + { + if (Settings.useCachedFiles) + { + System.out.println("Getting missing tmd from Server!"); + } tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray(titleId)); - }else{ + } else + { System.out.println("Downloading of missing files is not enabled. Exiting"); throw new ExitException("TMD missing."); } - } - - if(key != null){ + } + + if (key != null) + { System.out.println("Using ticket from parameter."); - ticket = new TIK(key,titleId); - }else{ - if(Settings.useCachedFiles){ + ticket = new TIK(key, titleId); + } else + { + if (Settings.useCachedFiles) + { File f = new File(getContentPath() + "/" + "cetk"); - if(f.exists()){ + if (f.exists()) + { System.out.println("Using cached cetk."); - ticket = new TIK(f,titleId); - }else{ + ticket = new TIK(f, titleId); + } else + { System.out.println("No cached ticket found."); } } - if(ticket == null){ - if(Settings.downloadWhenCachedFilesMissingOrBroken){ - if(Settings.useCachedFiles) System.out.println("getting missing ticket"); - ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(titleId),tmd.titleID); - }else{ + if (ticket == null) + { + if (Settings.downloadWhenCachedFilesMissingOrBroken) + { + if (Settings.useCachedFiles) + { + System.out.println("getting missing ticket"); + } + ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(titleId), tmd.titleID); + } else + { System.out.println("Downloading of missing files is not enabled. Exiting"); throw new ExitException("Ticket missing."); } } } - - if(Settings.downloadContent){ + + if (Settings.downloadContent) + { File f = new File(getContentPath() + "/" + String.format("%08x", tmd.contents[0].ID) + ".app"); - if(!(f.exists() && Settings.skipExistingFiles)){ + if (!(f.exists() && Settings.skipExistingFiles)) + { System.out.println("Downloading FST (" + String.format("%08x", tmd.contents[0].ID) + ")"); - Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath()); - }else{ - if(f.length() != tmd.contents[0].size){ - if(Settings.downloadWhenCachedFilesMissingOrBroken){ + Downloader.getInstance().downloadContent(titleId, tmd.contents[0].ID, getContentPath()); + } else + { + if (f.length() != tmd.contents[0].size) + { + if (Settings.downloadWhenCachedFilesMissingOrBroken) + { System.out.println("FST already existing, but broken. Downloading it again."); - Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath()); - }else{ + Downloader.getInstance().downloadContent(titleId, tmd.contents[0].ID, getContentPath()); + } else + { System.out.println("FST already existing, but broken. No download allowed."); throw new ExitException("FST missing."); - } - }else{ + } + } else + { System.out.println("Skipped download of FST. Already existing"); } - + } - + } - - Decryption decryption = new Decryption(ticket.getDecryptedKey(),0); + + Decryption decryption = new Decryption(ticket.getDecryptedKey(), 0); byte[] encryptedFST = null; - if(Settings.useCachedFiles){ + if (Settings.useCachedFiles) + { String path = getContentPath() + "/" + String.format("%08x", tmd.contents[0].ID) + ".app"; - File f = new File(path); - if(f.exists()){ + File f = new File(path); + if (f.exists()) + { System.out.println("Using cached FST"); Path file = Paths.get(path); encryptedFST = Files.readAllBytes(file); - }else{ - System.out.println("No cached FST (" + String.format("%08x", tmd.contents[0].ID) + ") found."); + } else + { + System.out.println("No cached FST (" + String.format("%08x", tmd.contents[0].ID) + ") found."); } } - if(encryptedFST == null){ - if(Settings.downloadWhenCachedFilesMissingOrBroken){ - if(Settings.useCachedFiles)System.out.println("Getting FST from server."); - encryptedFST = Downloader.getInstance().downloadContentToByteArray(titleId,tmd.contents[0].ID); - }else{ + if (encryptedFST == null) + { + if (Settings.downloadWhenCachedFilesMissingOrBroken) + { + if (Settings.useCachedFiles) + { + System.out.println("Getting FST from server."); + } + encryptedFST = Downloader.getInstance().downloadContentToByteArray(titleId, tmd.contents[0].ID); + } else + { System.out.println("Downloading of missing files is not enabled. Exiting"); throw new ExitException(""); } - } + } byte[] decryptedFST = decryption.decrypt(encryptedFST); - - fst = new FST(decryptedFST,tmd); + + fst = new FST(decryptedFST, tmd); tmd.setNUSTitle(this); - - if(Settings.downloadContent){ + + if (Settings.downloadContent) + { tmd.downloadContents(); } - - System.out.println("Total Size of Content Files: " + ((int)((getTotalContentSize()/1024.0/1024.0)*100))/100.0 +" MB"); - System.out.println("Total Size of Decrypted Files: " + ((int)((fst.getTotalContentSizeInNUS()/1024.0/1024.0)*100))/100.0 +" MB"); + + System.out.println("Total Size of Content Files: " + ((int) ((getTotalContentSize() / 1024.0 / 1024.0) * 100)) / 100.0 + " MB"); + System.out.println("Total Size of Decrypted Files: " + ((int) ((fst.getTotalContentSizeInNUS() / 1024.0 / 1024.0) * 100)) / 100.0 + " MB"); System.out.println("Entries: " + fst.getTotalEntries()); System.out.println("Entries: " + fst.getFileCount()); System.out.println("Files in NUSTitle: " + fst.getFileCountInNUS()); - - } catch (IOException e) { - // TODO Auto-generated catch block + + } catch (IOException e) + { e.printStackTrace(); } } - - - - - - public FST getFst() { + public FST getFst() + { return fst; } - - public void setFst(FST fst) { - this.fst = fst; - } - - public TitleMetaData getTmd() { - return tmd; - } - - public void setTmd(TitleMetaData tmd) { - this.tmd = tmd; - } - - public TIK getTicket() { + public TIK getTicket() + { return ticket; } - - - public void setTicket(TIK ticket) { - this.ticket = ticket; - } - - public long getTotalContentSize() { + public long getTotalContentSize() + { return tmd.getTotalContentSize(); } - - - public String getContentPath() { + public String getContentPath() + { return getContentPathPrefix() + String.format("%016X", getTitleID()); } - - public String getContentPathPrefix() { + + public String getContentPathPrefix() + { return "tmp_"; } - - - private long getTitleID() { + private long getTitleID() + { return titleID; } - - private void setTitleID(long titleId) { - this.titleID = titleId; + + private void setTitleID(long titleId) + { + this.titleID = titleId; } - - - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/Starter.java b/src/de/mas/jnustool/Starter.java deleted file mode 100644 index b3f4a54..0000000 --- a/src/de/mas/jnustool/Starter.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.mas.jnustool; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; - -import de.mas.jnustool.gui.NUSGUI; -import de.mas.jnustool.util.Downloader; -import de.mas.jnustool.util.ExitException; -import de.mas.jnustool.util.Util; - -public class Starter { - - public static void main(String[] args) { - System.out.println("JNUSTool 0.0.2 - pre alpha - by Maschell"); - System.out.println(); - try { - readConfig(); - } catch (IOException e) { - System.err.println("Error while reading config! Needs to be:"); - System.err.println("DOWNLOAD URL BASE"); - System.err.println("COMMONKEY"); - return; - } - if(args.length != 0){ - long titleID = Util.StringToLong(args[0]); - String key = null; - if( args.length > 1 && args[1].length() == 32){ - key = args[1].substring(0, 32); - } - NUSGUI m; - try { - m = new NUSGUI(new NUSTitle(titleID, key), null); - } catch (ExitException e) { - System.out.println("Error: " + e.getMessage()); - return; - } - m.setVisible(true); - }else{ - System.out.println("Need parameters: TITLEID [KEY]"); - } - - } - - public static void readConfig() throws IOException { - BufferedReader in = new BufferedReader(new FileReader(new File("config"))); - Downloader.URL_BASE = in.readLine(); - Util.commonKey = Util.hexStringToByteArray(in.readLine()); - in.close(); - - } - -} diff --git a/src/de/mas/jnustool/TIK.java b/src/de/mas/jnustool/TIK.java index 76c0aed..c54a18e 100644 --- a/src/de/mas/jnustool/TIK.java +++ b/src/de/mas/jnustool/TIK.java @@ -5,67 +5,63 @@ import java.io.IOException; import java.io.RandomAccessFile; import de.mas.jnustool.util.Decryption; -import de.mas.jnustool.util.Util; +import de.mas.jnustool.util.ConversionUtils; -public class TIK { - public static int KEY_LENGTH = 16; - private byte[] encryptedKey = new byte[16]; - private byte[] decryptedKey = new byte[16]; - - public TIK(File cetk,long titleid) throws IOException{ - parse(cetk); - calculateDecryptedKey(titleid); - } +public class TIK +{ + private byte[] encryptedKey = new byte[16]; + private byte[] decryptedKey = new byte[16]; - public TIK(String ticketKey,long titleid) { - setEncryptedKey(ticketKey); - calculateDecryptedKey(titleid); - } - + public TIK(File commonETicket, long titleID) throws IOException + { + parse(commonETicket); + calculateDecryptedKey(titleID); + } - public TIK(byte[] file, long titleID) throws IOException { - parse(file); - calculateDecryptedKey(titleID); - } + public TIK(String ticketKey, long titleID) + { + setEncryptedKey(ticketKey); + calculateDecryptedKey(titleID); + } - private void calculateDecryptedKey(long titleid) { - Decryption decryption = new Decryption(Util.commonKey,titleid); - decryptedKey = decryption.decrypt(encryptedKey); - } + public TIK(byte[] file, long titleID) throws IOException + { + parse(file); + calculateDecryptedKey(titleID); + } - private void parse(byte[] cetk) throws IOException { - System.arraycopy(cetk, 0x1bf, this.encryptedKey, 0,16); - } - - private void parse(File cetk) throws IOException { - RandomAccessFile f = new RandomAccessFile(cetk, "r"); - f.seek(0x1bf); - f.read(this.encryptedKey, 0, 16); - f.close(); - } - - public void setEncryptedKey(String key) { - this.encryptedKey = Util.hexStringToByteArray(key); - } - - public byte[] getEncryptedKey() { - return encryptedKey; - } + private void calculateDecryptedKey(long titleID) + { + Decryption decryption = new Decryption(ConversionUtils.commonKey, titleID); + decryptedKey = decryption.decrypt(encryptedKey); + } - public void setEncryptedKey(byte[] encryptedKey) { - this.encryptedKey = encryptedKey; - } + private void parse(byte[] commonETicketBytes) throws IOException + { + System.arraycopy(commonETicketBytes, 0x1bf, this.encryptedKey, 0, 16); + } - public byte[] getDecryptedKey() { - return decryptedKey; - } + private void parse(File commonETicket) throws IOException + { + RandomAccessFile f = new RandomAccessFile(commonETicket, "r"); + f.seek(0x1bf); + f.read(this.encryptedKey, 0, 16); + f.close(); + } - public void setDecryptedKey(byte[] decryptedKey) { - this.decryptedKey = decryptedKey; - } + public void setEncryptedKey(String key) + { + this.encryptedKey = ConversionUtils.hexStringToByteArray(key); + } - @Override - public String toString(){ - return "encrypted key: " + Util.ByteArrayToString(encryptedKey)+ " decrypted key: " + Util.ByteArrayToString(decryptedKey); - } -} + public byte[] getDecryptedKey() + { + return decryptedKey; + } + + @Override + public String toString() + { + return "encrypted key: " + ConversionUtils.ByteArrayToString(encryptedKey) + " decrypted key: " + ConversionUtils.ByteArrayToString(decryptedKey); + } +} \ No newline at end of file diff --git a/src/de/mas/jnustool/TitleMetaData.java b/src/de/mas/jnustool/TitleMetaData.java index 908348c..90993c8 100644 --- a/src/de/mas/jnustool/TitleMetaData.java +++ b/src/de/mas/jnustool/TitleMetaData.java @@ -3,73 +3,73 @@ package de.mas.jnustool; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import de.mas.jnustool.util.Downloader; import de.mas.jnustool.util.ExitException; import de.mas.jnustool.util.Settings; -import de.mas.jnustool.util.Util; +import de.mas.jnustool.util.ConversionUtils; + +public class TitleMetaData +{ + int signatureType; // 0x000 + byte[] signature = new byte[0x100]; // 0x004 + byte[] issuer = new byte[0x40]; // 0x140 + byte version; // 0x180 + byte CACRLVersion; // 0x181 + byte signerCRLVersion; // 0x182 + long systemVersion; // 0x184 + long titleID; // 0x18C + int titleType; // 0x194 + short groupID; // 0x198 + byte[] reserved = new byte[62]; // 0x19A + int accessRights; // 0x1D8 + short titleVersion; // 0x1DC + short contentCount; // 0x1DE + short bootIndex; // 0x1E0 + byte[] SHA2 = new byte[32]; // 0x1E4 + ContentInfo[] contentInfoArray = new ContentInfo[64]; // 0x1E4 + Content[] contents; // 0x1E4 + -public class TitleMetaData { - int signatureType; // 0x000 - byte[] signature = new byte[0x100]; // 0x004 - byte[] issuer = new byte[0x40]; // 0x140 - byte version; // 0x180 - byte CACRLVersion; // 0x181 - byte signerCRLVersion; // 0x182 - long systemVersion; // 0x184 - long titleID; // 0x18C - int titleType; // 0x194 - short groupID; // 0x198 - byte[] reserved = new byte[62]; // 0x19A - int accessRights; // 0x1D8 - short titleVersion; // 0x1DC - short contentCount; // 0x1DE - short bootIndex; // 0x1E0 - byte[] SHA2 = new byte[32]; // 0x1E4 - ContentInfo[] contentInfos = new ContentInfo[64]; // 0x1E4 - Content[] contents; // 0x1E4 - - private NUSTitle nus; - + private long totalContentSize; - - public TitleMetaData(File tmd) throws IOException { + + public TitleMetaData(File tmd) throws IOException + { parse(tmd); setTotalContentSize(); } - public TitleMetaData(byte[] downloadTMDToByteArray) throws IOException { - if(downloadTMDToByteArray != null){ - File tempFile = File.createTempFile("bla","blubb"); + public TitleMetaData(byte[] downloadTMDToByteArray) throws IOException + { + if (downloadTMDToByteArray != null) + { + File tempFile = File.createTempFile("bla", "blubb"); FileOutputStream fos = new FileOutputStream(tempFile); fos.write(downloadTMDToByteArray); fos.close(); parse(tempFile); setTotalContentSize(); - }else{ + } else + { System.err.println("Invalid TMD"); throw new IllegalArgumentException("Invalid TMD"); } } - private void parse(File tmd) throws IOException { - RandomAccessFile f = new RandomAccessFile(tmd, "r"); + private void parse(File tmd) throws IOException + { + RandomAccessFile f = new RandomAccessFile(tmd, "r"); f.seek(0); this.signatureType = f.readInt(); - + f.read(signature, 0, 0x100); f.seek(0x140); f.read(issuer, 0, 0x40); - + f.seek(0x180); this.version = f.readByte(); this.CACRLVersion = f.readByte(); @@ -89,123 +89,148 @@ public class TitleMetaData { f.seek(0x1E4); f.read(SHA2, 0, 32); f.seek(0x204); - + short indexOffset; short commandCount; - - for(int i =0;i<64;i++){ - f.seek(0x204+(0x24*i)); - indexOffset =f.readShort(); - commandCount =f.readShort(); - byte[] buffer = new byte[0x20]; // 16 0xB14 + + for (int i = 0; i < 64; i++) + { + f.seek(0x204 + (0x24 * i)); + indexOffset = f.readShort(); + commandCount = f.readShort(); + byte[] buffer = new byte[0x20]; // 16 0xB14 f.read(buffer, 0, 0x20); - this.contentInfos[i] = new ContentInfo(indexOffset,commandCount,buffer); + this.contentInfoArray[i] = new ContentInfo(indexOffset, commandCount, buffer); } this.contents = new Content[contentCount]; - - int ID; // 0 0xB04 - short index; // 4 0xB08 - short type; // 6 0xB0A - long size; // 8 0xB0C - - - for(int i =0;i nodesCheckingState; + HashSet checkedPaths = new HashSet(); + + // Defining a new event type for the checking mechanism and preparing event-handling mechanism + protected EventListenerList listenerList = new EventListenerList(); + + public class CheckChangeEvent extends EventObject + { + private static final long serialVersionUID = -8100230309044193368L; + + public CheckChangeEvent(Object source) + { + super(source); + } + } + + public interface CheckChangeEventListener extends EventListener + { + void checkStateChanged(CheckChangeEvent event); + } + + void fireCheckChangeEvent(CheckChangeEvent evt) + { + Object[] listeners = listenerList.getListenerList(); + for (int i = 0; i < listeners.length; i++) + { + if (listeners[i] == CheckChangeEventListener.class) + { + ((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt); + } + } + } + + // Override + public void setModel(TreeModel newModel) + { + super.setModel(newModel); + resetCheckingState(); + } + + // New method that returns only the checked paths (totally ignores original "selection" mechanism) + public TreePath[] getCheckedPaths() + { + return checkedPaths.toArray(new TreePath[checkedPaths.size()]); + } + + private void resetCheckingState() + { + nodesCheckingState = new HashMap<>(); + checkedPaths = new HashSet<>(); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) getModel().getRoot(); + if (node == null) + { + return; + } + addSubtreeToCheckingStateTracking(node); + } + + // Creating data structure of the current model for the checking mechanism + private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) + { + TreeNode[] path = node.getPath(); + TreePath tp = new TreePath(path); + CheckedNode cn = new CheckedNode(false, node.getChildCount() > 0, false); + nodesCheckingState.put(tp, cn); + for (int i = 0; i < node.getChildCount(); i++) + { + addSubtreeToCheckingStateTracking((DefaultMutableTreeNode) tp.pathByAddingChild(node.getChildAt(i)).getLastPathComponent()); + } + } + + // Overriding cell renderer by a class that ignores the original "selection" mechanism + // It decides how to show the nodes due to the checking-mechanism + private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer + { + private static final long serialVersionUID = -7341833835878991719L; + JCheckBox checkBox; + + public CheckBoxCellRenderer() + { + super(); + this.setLayout(new BorderLayout()); + checkBox = new JCheckBox(); + add(checkBox, BorderLayout.CENTER); + setOpaque(false); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean selected, boolean expanded, boolean leaf, int row, + boolean hasFocus) + { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + Object obj = node.getUserObject(); + TreePath tp = new TreePath(node.getPath()); + CheckedNode cn = nodesCheckingState.get(tp); + if (cn == null) + { + return this; + } + checkBox.setSelected(cn.isSelected); + if (obj instanceof FEntry) + { + FEntry f = (FEntry) obj; + checkBox.setText(f.getFileName()); + } else + { + checkBox.setText(obj.toString()); + } + + checkBox.setOpaque(cn.isSelected && cn.hasChildren && !cn.allChildrenSelected); + return this; + } + } + + public JCheckBoxTree(NUSTitle nus) + { + super(); + setModel(new DefaultTreeModel(nus.getFst().getFSTDirectory().getNodes())); - // Defining data structure that will enable to fast check-indicate the state of each node - // It totally replaces the "selection" mechanism of the JTree - private class CheckedNode { - boolean isSelected; - boolean hasChildren; - boolean allChildrenSelected; + // Disabling toggling by double-click + this.setToggleClickCount(0); + // Overriding cell renderer by new one defined above + CheckBoxCellRenderer cellRenderer = new CheckBoxCellRenderer(); + this.setCellRenderer(cellRenderer); - public CheckedNode(boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) { - isSelected = isSelected_; - hasChildren = hasChildren_; - allChildrenSelected = allChildrenSelected_; - } - } - HashMap nodesCheckingState; - HashSet checkedPaths = new HashSet(); + // Overriding selection model by an empty one + DefaultTreeSelectionModel defaultTreeSelectionModel = new DefaultTreeSelectionModel() + { + private static final long serialVersionUID = -8190634240451667286L; - // Defining a new event type for the checking mechanism and preparing event-handling mechanism - protected EventListenerList listenerList = new EventListenerList(); + // Totally disabling the selection mechanism + public void setSelectionPath(TreePath path) + { + } - public class CheckChangeEvent extends EventObject { - private static final long serialVersionUID = -8100230309044193368L; + public void addSelectionPath(TreePath path) + { + } - public CheckChangeEvent(Object source) { - super(source); - } - } + public void removeSelectionPath(TreePath path) + { + } - public interface CheckChangeEventListener extends EventListener { - public void checkStateChanged(CheckChangeEvent event); - } + public void setSelectionPaths(TreePath[] pPaths) + { + } + }; - public void addCheckChangeEventListener(CheckChangeEventListener listener) { - listenerList.add(CheckChangeEventListener.class, listener); - } - public void removeCheckChangeEventListener(CheckChangeEventListener listener) { - listenerList.remove(CheckChangeEventListener.class, listener); - } + // Calling checking mechanism on mouse click + this.addMouseListener(new MouseListener() + { + public void mouseClicked(MouseEvent arg0) + { + TreePath tp = selfPointer.getPathForLocation(arg0.getX(), arg0.getY()); + if (tp == null) + { + return; + } + boolean checkMode = !nodesCheckingState.get(tp).isSelected; + checkSubTree(tp, checkMode); + updatePredecessorsWithCheckMode(tp, checkMode); + // Firing the check change event + fireCheckChangeEvent(new CheckChangeEvent(new Object())); + // Repainting tree after the data structures were updated + selfPointer.repaint(); + } - void fireCheckChangeEvent(CheckChangeEvent evt) { - Object[] listeners = listenerList.getListenerList(); - for (int i = 0; i < listeners.length; i++) { - if (listeners[i] == CheckChangeEventListener.class) { - ((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt); - } - } - } + public void mouseEntered(MouseEvent arg0) + { + } - // Override - public void setModel(TreeModel newModel) { - super.setModel(newModel); - resetCheckingState(); - } + public void mouseExited(MouseEvent arg0) + { + } - // New method that returns only the checked paths (totally ignores original "selection" mechanism) - public TreePath[] getCheckedPaths() { - return checkedPaths.toArray(new TreePath[checkedPaths.size()]); - } + public void mousePressed(MouseEvent arg0) + { + } - // Returns true in case that the node is selected, has children but not all of them are selected - public boolean isSelectedPartially(TreePath path) { - CheckedNode cn = nodesCheckingState.get(path); - return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected; - } + public void mouseReleased(MouseEvent arg0) + { + } + }); - private void resetCheckingState() { - nodesCheckingState = new HashMap(); - checkedPaths = new HashSet(); - DefaultMutableTreeNode node = (DefaultMutableTreeNode)getModel().getRoot(); - if (node == null) { - return; - } - addSubtreeToCheckingStateTracking(node); - } + this.setSelectionModel(defaultTreeSelectionModel); + } - // Creating data structure of the current model for the checking mechanism - private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) { - TreeNode[] path = node.getPath(); - TreePath tp = new TreePath(path); - CheckedNode cn = new CheckedNode(false, node.getChildCount() > 0, false); - nodesCheckingState.put(tp, cn); - for (int i = 0 ; i < node.getChildCount() ; i++) { - addSubtreeToCheckingStateTracking((DefaultMutableTreeNode) tp.pathByAddingChild(node.getChildAt(i)).getLastPathComponent()); - } - } - - // Overriding cell renderer by a class that ignores the original "selection" mechanism - // It decides how to show the nodes due to the checking-mechanism - private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer { - private static final long serialVersionUID = -7341833835878991719L; - JCheckBox checkBox; - public CheckBoxCellRenderer() { - super(); - this.setLayout(new BorderLayout()); - checkBox = new JCheckBox(); - add(checkBox, BorderLayout.CENTER); - setOpaque(false); - } - - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, - boolean selected, boolean expanded, boolean leaf, int row, - boolean hasFocus) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode)value; - Object obj = node.getUserObject(); - TreePath tp = new TreePath(node.getPath()); - CheckedNode cn = nodesCheckingState.get(tp); - if (cn == null) { - return this; - } - checkBox.setSelected(cn.isSelected); - if(obj instanceof FEntry){ - FEntry f = (FEntry) obj; - checkBox.setText(f.getFileName()); - }else{ - checkBox.setText(obj.toString()); - } - - checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected); - return this; - } - } - - public JCheckBoxTree(NUSTitle nus) { - super(); - - - - setModel(new DefaultTreeModel(nus.getFst().getFSTDirectory().getNodes())); - - // Disabling toggling by double-click - this.setToggleClickCount(0); - // Overriding cell renderer by new one defined above - CheckBoxCellRenderer cellRenderer = new CheckBoxCellRenderer(); - this.setCellRenderer(cellRenderer); - - // Overriding selection model by an empty one - DefaultTreeSelectionModel dtsm = new DefaultTreeSelectionModel() { - private static final long serialVersionUID = -8190634240451667286L; - // Totally disabling the selection mechanism - public void setSelectionPath(TreePath path) { - } - public void addSelectionPath(TreePath path) { - } - public void removeSelectionPath(TreePath path) { - } - public void setSelectionPaths(TreePath[] pPaths) { - } - }; - - - - // Calling checking mechanism on mouse click - this.addMouseListener(new MouseListener() { - public void mouseClicked(MouseEvent arg0) { - TreePath tp = selfPointer.getPathForLocation(arg0.getX(), arg0.getY()); - if (tp == null) { - return; - } - boolean checkMode = ! nodesCheckingState.get(tp).isSelected; - checkSubTree(tp, checkMode); - updatePredecessorsWithCheckMode(tp, checkMode); - // Firing the check change event - fireCheckChangeEvent(new CheckChangeEvent(new Object())); - // Repainting tree after the data structures were updated - selfPointer.repaint(); - } - public void mouseEntered(MouseEvent arg0) { - } - public void mouseExited(MouseEvent arg0) { - } - public void mousePressed(MouseEvent arg0) { - } - public void mouseReleased(MouseEvent arg0) { - } - }); - - this.setSelectionModel(dtsm); - } - - // When a node is checked/unchecked, updating the states of the predecessors - protected void updatePredecessorsWithCheckMode(TreePath tp, boolean check) { - TreePath parentPath = tp.getParentPath(); - // If it is the root, stop the recursive calls and return - if (parentPath == null) { - return; - } - CheckedNode parentCheckedNode = nodesCheckingState.get(parentPath); - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) parentPath.getLastPathComponent(); - parentCheckedNode.allChildrenSelected = true; - parentCheckedNode.isSelected = false; - for (int i = 0 ; i < parentNode.getChildCount() ; i++) { - TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i)); - CheckedNode childCheckedNode = nodesCheckingState.get(childPath); - // It is enough that even one subtree is not fully selected - // to determine that the parent is not fully selected - if (! childCheckedNode.allChildrenSelected) { - parentCheckedNode.allChildrenSelected = false; - } - // If at least one child is selected, selecting also the parent - if (childCheckedNode.isSelected) { - parentCheckedNode.isSelected = true; - } - } - if (parentCheckedNode.isSelected) { - checkedPaths.add(parentPath); - } else { - checkedPaths.remove(parentPath); - } - // Go to upper predecessor - updatePredecessorsWithCheckMode(parentPath, check); - } - - // Recursively checks/unchecks a subtree - protected void checkSubTree(TreePath tp, boolean check) { - CheckedNode cn = nodesCheckingState.get(tp); - cn.isSelected = check; - DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent(); - for (int i = 0 ; i < node.getChildCount() ; i++) { - checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check); - } - cn.allChildrenSelected = check; - if (check) { - checkedPaths.add(tp); - } else { - checkedPaths.remove(tp); - } - } + // When a node is checked/unchecked, updating the states of the predecessors + protected void updatePredecessorsWithCheckMode(TreePath tp, boolean check) + { + TreePath parentPath = tp.getParentPath(); + // If it is the root, stop the recursive calls and return + if (parentPath == null) + { + return; + } + CheckedNode parentCheckedNode = nodesCheckingState.get(parentPath); + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) parentPath.getLastPathComponent(); + parentCheckedNode.allChildrenSelected = true; + parentCheckedNode.isSelected = false; + for (int i = 0; i < parentNode.getChildCount(); i++) + { + TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i)); + CheckedNode childCheckedNode = nodesCheckingState.get(childPath); + // It is enough that even one subtree is not fully selected + // to determine that the parent is not fully selected + if (!childCheckedNode.allChildrenSelected) + { + parentCheckedNode.allChildrenSelected = false; + } + // If at least one child is selected, selecting also the parent + if (childCheckedNode.isSelected) + { + parentCheckedNode.isSelected = true; + } + } + if (parentCheckedNode.isSelected) + { + checkedPaths.add(parentPath); + } else + { + checkedPaths.remove(parentPath); + } + // Go to upper predecessor + updatePredecessorsWithCheckMode(parentPath, check); + } + // Recursively checks/un-checks a subtree + protected void checkSubTree(TreePath tp, boolean check) + { + CheckedNode cn = nodesCheckingState.get(tp); + cn.isSelected = check; + DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent(); + for (int i = 0; i < node.getChildCount(); i++) + { + checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check); + } + cn.allChildrenSelected = check; + if (check) + { + checkedPaths.add(tp); + } else + { + checkedPaths.remove(tp); + } + } } \ No newline at end of file diff --git a/src/de/mas/jnustool/gui/NUSGUI.java b/src/de/mas/jnustool/gui/NUSGUI.java deleted file mode 100644 index 5c061f7..0000000 --- a/src/de/mas/jnustool/gui/NUSGUI.java +++ /dev/null @@ -1,80 +0,0 @@ -package de.mas.jnustool.gui; - -import java.awt.BorderLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ForkJoinPool; - -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.JScrollPane; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.TreePath; - -import de.mas.jnustool.FEntry; -import de.mas.jnustool.NUSTitle; -import de.mas.jnustool.util.Settings; -import de.mas.jnustool.FEntryDownloader; - -public class NUSGUI extends JFrame { - - private static final long serialVersionUID = 4648172894076113183L; - - public NUSGUI(NUSTitle nus,Settings mode) { - super(); - setSize(800, 600); - getContentPane().setLayout(new BorderLayout(0, 0)); - - final JCheckBoxTree cbt = new JCheckBoxTree(nus); - JScrollPane qPane = new JScrollPane(cbt, - JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, - JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - this.getContentPane().add(qPane); - - - JButton btnNewButton = new JButton("Download"); - btnNewButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - new Thread(new Runnable() { public void run() { - ForkJoinPool pool = ForkJoinPool.commonPool(); - List list = new ArrayList<>(); - - - TreePath[] paths = cbt.getCheckedPaths(); - for (TreePath tp : paths) { - - Object obj = tp.getPath()[tp.getPath().length-1]; - if(((DefaultMutableTreeNode)obj).getUserObject() instanceof FEntry){ - FEntry f = (FEntry) ((DefaultMutableTreeNode)obj).getUserObject(); - if(!f.isDir() && f.isInNUSTitle()){ - list.add(new FEntryDownloader(f)); - } - - } - } - pool.invokeAll(list); - System.out.println("Done!"); - }}).start(); - - } - }); - getContentPane().add(btnNewButton, BorderLayout.SOUTH); - - /*cbt.addCheckChangeEventListener(new JCheckBoxTree.CheckChangeEventListener() { - public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) { - System.out.println("event"); - TreePath[] paths = cbt.getCheckedPaths(); - for (TreePath tp : paths) { - for (Object pathPart : tp.getPath()) { - System.out.print(pathPart + ","); - } - System.out.println(); - } - } - });*/ - - this.setDefaultCloseOperation(EXIT_ON_CLOSE); - } -} \ No newline at end of file diff --git a/src/de/mas/jnustool/util/Decryption.java b/src/de/mas/jnustool/util/Decryption.java index dfd59b4..5e92d0e 100644 --- a/src/de/mas/jnustool/util/Decryption.java +++ b/src/de/mas/jnustool/util/Decryption.java @@ -1,15 +1,7 @@ package de.mas.jnustool.util; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; +import de.mas.jnustool.FEntry; +import de.mas.jnustool.TIK; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -17,354 +9,399 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; -import de.mas.jnustool.FEntry; -import de.mas.jnustool.TIK; +public class Decryption +{ + private Cipher cipher2; + private byte[] decryptedKey; -public class Decryption { - Cipher cipher2; - - public Decryption(TIK ticket){ + public Decryption(TIK ticket) + { this(ticket.getDecryptedKey()); } - - public Decryption(byte[] decryptedKey){ - this(decryptedKey,0); + + public Decryption(byte[] decryptedKey) + { + this(decryptedKey, 0); } - - public Decryption(byte[] decryptedKey, long titleId) { - try { + + public Decryption(byte[] decryptedKey, long titleId) + { + try + { cipher2 = Cipher.getInstance("AES/CBC/NoPadding"); this.decryptedKey = decryptedKey; init(titleId); - - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchPaddingException e) { - // TODO Auto-generated catch block + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) + { e.printStackTrace(); } } + private void init(byte[] IV) + { + init(decryptedKey, IV); + } - byte[] decryptedKey; - - - private void init(byte[] IV) { - init(decryptedKey,IV); + private void init(long titleID) + { + init(ByteBuffer.allocate(16).putLong(titleID).array()); } - - private void init(long titleid) { - init(ByteBuffer.allocate(16).putLong(titleid).array()); - } - - public void init(byte[] decryptedKey,long titleid){ - init(decryptedKey,ByteBuffer.allocate(16).putLong(titleid).array()); - } - - public void init(byte[] decryptedKey,byte[] iv){ - try { - this.decryptedKey = decryptedKey; + + public void init(byte[] decryptedKey, byte[] iv) + { + try + { + this.decryptedKey = decryptedKey; SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES"); - cipher2.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); - } catch (Exception e) { - // TODO Auto-generated catch block + cipher2.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); + } catch (Exception e) + { e.printStackTrace(); } } - - public byte[] decrypt(byte[] input){ - try { - return cipher2.doFinal(input); - } catch (IllegalBlockSizeException | BadPaddingException e) { - // TODO Auto-generated catch block + + public byte[] decrypt(byte[] input) + { + try + { + return cipher2.doFinal(input); + } catch (IllegalBlockSizeException | BadPaddingException e) + { e.printStackTrace(); } - return input; + return input; } - - public byte[] decrypt(byte[] input,int len){ - return decrypt(input,0,len); - } - - public byte[] decrypt(byte[] input,int offset,int len){ - try { - return cipher2.doFinal(input, offset, len); - } catch (IllegalBlockSizeException | BadPaddingException e) { - // TODO Auto-generated catch block + + public byte[] decrypt(byte[] input, int offset, int len) + { + try + { + return cipher2.doFinal(input, offset, len); + } catch (IllegalBlockSizeException | BadPaddingException e) + { e.printStackTrace(); } - return input; + return input; } - + byte[] IV; - public byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) { - return decryptFileChunk(blockBuffer,0,BLOCKSIZE, IV); + + public byte[] decryptFileChunk(byte[] blockBuffer, int blockSize, byte[] IV) + { + return decryptFileChunk(blockBuffer, 0, blockSize, IV); } - - public byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) { - if(IV != null) this.IV = IV; + + public byte[] decryptFileChunk(byte[] blockBuffer, int offset, int blockSize, byte[] IV) + { + if (IV != null) + { + this.IV = IV; + } init(this.IV); - byte[] output = decrypt(blockBuffer,offset,BLOCKSIZE); - this.IV = Arrays.copyOfRange(blockBuffer,BLOCKSIZE-16, BLOCKSIZE); + byte[] output = decrypt(blockBuffer, offset, blockSize); + this.IV = Arrays.copyOfRange(blockBuffer, blockSize - 16, blockSize); return output; } - byte[] hash = new byte[20]; - byte[] h0 = new byte[20]; - - public byte[] decryptFileChunkHash(byte[] blockBuffer, int BLOCKSIZE, int block, int contentID){ - if(BLOCKSIZE != 0x10000) throw new IllegalArgumentException("Blocksize not supported"); + byte[] hash = new byte[20]; + byte[] h0 = new byte[20]; + + public byte[] decryptFileChunkHash(byte[] blockBuffer, int blockSize, int block, int contentID) + { + if (blockSize != 0x10000) + { + throw new IllegalArgumentException("Block size not supported"); + } IV = new byte[16]; - IV[1] = (byte)contentID; - - byte[] hashes = decryptFileChunk(blockBuffer,0x0400,IV); - - System.arraycopy(hashes, (int) (0x14*block), IV, 0, 16); - System.arraycopy(hashes, (int) (0x14*block), h0, 0, 20); - - if( block == 0 ) - IV[1] ^= (byte)contentID; - - byte[] output = decryptFileChunk(blockBuffer,0x400,0xFC00,IV); - + IV[1] = (byte) contentID; + + byte[] hashes = decryptFileChunk(blockBuffer, 0x0400, IV); + + System.arraycopy(hashes, 0x14 * block, IV, 0, 16); + System.arraycopy(hashes, 0x14 * block, h0, 0, 20); + + if (block == 0) + { + IV[1] ^= (byte) contentID; + } + + byte[] output = decryptFileChunk(blockBuffer, 0x400, 0xFC00, IV); + hash = hash(output); - if(block == 0){ - + if (block == 0) + { + assert hash != null; hash[1] ^= contentID; - } - if(Arrays.equals(hash, h0)){ + if (Arrays.equals(hash, h0)) + { //System.out.println("checksum right"); - } - else{ + } else + { System.out.println("checksum failed"); - System.out.println(Util.ByteArrayToString(hash)); - System.out.println(Util.ByteArrayToString(h0)); + System.out.println(ConversionUtils.ByteArrayToString(hash)); + System.out.println(ConversionUtils.ByteArrayToString(h0)); throw new IllegalArgumentException("checksumfail"); } return output; } - - public static byte[] hash(byte[] hashThis) { - try { - byte[] hash = new byte[20]; - MessageDigest md = MessageDigest.getInstance("SHA-1"); - hash = md.digest(hashThis); - return hash; - } catch (NoSuchAlgorithmException nsae) { - System.err.println("SHA-1 algorithm is not available..."); - System.exit(2); - } - return null; - } - - - public void decryptFile(InputStream inputSteam, OutputStream outputStream,FEntry toDownload) throws IOException{ - int BLOCKSIZE = 0x8000; - long dlFileLength = toDownload.getFileLength(); - if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){ - dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE; - } - - int bytesRead = -1; - - byte[] IV = new byte[16]; - IV[1] = (byte)toDownload.getContentID(); - - byte[] downloadBuffer; - - byte[] blockBuffer = new byte[BLOCKSIZE]; - byte[] overflowBuffer = new byte[BLOCKSIZE]; - int overflowsize = 0; - - int inBlockBuffer = 0; - byte[] tmp = new byte[BLOCKSIZE]; - boolean endd = false; - long downloadTotalsize = 0; - long wrote = 0; - - boolean first = true; - do{ - downloadBuffer = new byte[BLOCKSIZE-overflowsize]; - - bytesRead = inputSteam.read(downloadBuffer); - downloadTotalsize += bytesRead; - if(bytesRead ==-1){ - endd = true; - } - - if(!endd)System.arraycopy(downloadBuffer, 0, overflowBuffer, overflowsize,bytesRead); - - bytesRead += overflowsize; - - overflowsize = 0; - int oldInThisBlock = inBlockBuffer; - - if(oldInThisBlock + bytesRead > BLOCKSIZE){ - - int tooMuch = (oldInThisBlock + bytesRead) - BLOCKSIZE; - int toRead = BLOCKSIZE - oldInThisBlock; - - System.arraycopy(overflowBuffer, 0, blockBuffer, oldInThisBlock, toRead); - inBlockBuffer += toRead; - - overflowsize = tooMuch; - System.arraycopy(overflowBuffer, toRead, tmp, 0, tooMuch); - - System.arraycopy(tmp, 0, overflowBuffer, 0, tooMuch); - - - }else{ - if(!endd)System.arraycopy(overflowBuffer, 0, blockBuffer, inBlockBuffer, bytesRead); - inBlockBuffer +=bytesRead; - } - - if(inBlockBuffer == BLOCKSIZE || endd){ - if(first){ - first = false; - }else{ - IV = null; - } - - byte[] output = decryptFileChunk(blockBuffer,BLOCKSIZE,IV); - - if((wrote + inBlockBuffer) > toDownload.getFileLength()){ - inBlockBuffer = (int) (toDownload.getFileLength()- wrote); - } - - wrote += inBlockBuffer; - outputStream.write(output, 0, inBlockBuffer); - - inBlockBuffer = 0; - } - - }while(downloadTotalsize < dlFileLength && !endd); + public static byte[] hash(byte[] hashThis) + { + try + { + byte[] hash; + MessageDigest md = MessageDigest.getInstance("SHA-1"); - outputStream.close(); - inputSteam.close(); + hash = md.digest(hashThis); + return hash; + } catch (NoSuchAlgorithmException noSuchAlgorithm) + { + System.err.println("SHA-1 algorithm is not available..."); + System.exit(2); + } + return null; } - - public void decryptFileHash(InputStream inputSteam, OutputStream outputStream,FEntry toDownload) throws IOException{ - int BLOCKSIZE = 0x10000; - int HASHBLOCKSIZE = 0xFC00; - long writeSize = HASHBLOCKSIZE; // Hash block size - - long block = (toDownload.getFileOffset() / HASHBLOCKSIZE) & 0xF; - - long soffset = toDownload.getFileOffset() - (toDownload.getFileOffset() / HASHBLOCKSIZE * HASHBLOCKSIZE); - - long size = toDownload.getFileLength(); - - if( soffset+size > writeSize ) - writeSize = writeSize - soffset; - - int bytesRead = -1; - byte[] downloadBuffer; - - byte[] encryptedBlockBuffer = new byte[BLOCKSIZE]; - byte[] buffer = new byte[BLOCKSIZE]; - - int encryptedBytesInBuffer = 0; - int bufferPostion = 0; - - - byte[] tmp = new byte[BLOCKSIZE]; - boolean lastPart = false; - long wrote = 0; - - do{ - downloadBuffer = new byte[BLOCKSIZE-bufferPostion]; - bytesRead = inputSteam.read(downloadBuffer); - int bytesInBuffer = bytesRead + bufferPostion; - if(bytesRead ==-1){ - lastPart = true; - }else{ - System.arraycopy(downloadBuffer, 0, buffer, bufferPostion,bytesRead); //copy downloaded stuff in buffer - bufferPostion = 0; - } - - if(encryptedBytesInBuffer + bytesInBuffer > BLOCKSIZE){ - int tooMuch = (encryptedBytesInBuffer + bytesInBuffer) - BLOCKSIZE; - int toRead = BLOCKSIZE - encryptedBytesInBuffer; - - System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, toRead); // make buffer with encrypteddata full - encryptedBytesInBuffer += toRead; - - bufferPostion = tooMuch; //set buffer position; - System.arraycopy(buffer, toRead, tmp, 0, tooMuch); - System.arraycopy(tmp, 0, buffer, 0, tooMuch); - - }else{ - if(!lastPart) System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, bytesInBuffer); //When File if at the end, no more need to copy - encryptedBytesInBuffer +=bytesInBuffer; + + public void decryptFile(InputStream inputSteam, OutputStream outputStream, FEntry toDownload) throws IOException + { + int blockSize = 0x8000; + long dlFileLength = toDownload.getFileLength(); + if (dlFileLength > (dlFileLength / blockSize) * blockSize) + { + dlFileLength = ((dlFileLength / blockSize) * blockSize) + blockSize; + } + + int bytesRead; + + byte[] IV = new byte[16]; + IV[1] = (byte) toDownload.getContentID(); + + byte[] downloadBuffer; + + byte[] blockBuffer = new byte[blockSize]; + byte[] overflowBuffer = new byte[blockSize]; + int overflowSize = 0; + + int inBlockBuffer = 0; + byte[] tmp = new byte[blockSize]; + boolean end = false; + long totalDownloadSize = 0; + long wrote = 0; + + boolean first = true; + do + { + downloadBuffer = new byte[blockSize - overflowSize]; + bytesRead = inputSteam.read(downloadBuffer); + totalDownloadSize += bytesRead; + if (bytesRead == -1) + { + end = true; } - - //If downloaded BLOCKSIZE, or file at the end: Decrypt! - if(encryptedBytesInBuffer == BLOCKSIZE || lastPart){ - - if( writeSize > size ) - writeSize = size; - - byte[] output = decryptFileChunkHash(encryptedBlockBuffer, BLOCKSIZE, (int) block,toDownload.getContentID()); - - if((wrote + writeSize) > toDownload.getFileLength()){ - writeSize = (int) (toDownload.getFileLength()- wrote); - } - - outputStream.write(output, (int)(0+soffset), (int)writeSize); - wrote +=writeSize; - encryptedBytesInBuffer = 0; - - block++; - if( block >= 16 ) - block = 0; - - if( soffset > 0) + + if (!end) + { + System.arraycopy(downloadBuffer, 0, overflowBuffer, overflowSize, bytesRead); + } + + bytesRead += overflowSize; + + overflowSize = 0; + int oldInThisBlock = inBlockBuffer; + + if (oldInThisBlock + bytesRead > blockSize) + { + int tooMuch = (oldInThisBlock + bytesRead) - blockSize; + int toRead = blockSize - oldInThisBlock; + + System.arraycopy(overflowBuffer, 0, blockBuffer, oldInThisBlock, toRead); + inBlockBuffer += toRead; + + overflowSize = tooMuch; + System.arraycopy(overflowBuffer, toRead, tmp, 0, tooMuch); + + System.arraycopy(tmp, 0, overflowBuffer, 0, tooMuch); + } else + { + if (!end) { - writeSize = HASHBLOCKSIZE; - soffset = 0; + System.arraycopy(overflowBuffer, 0, blockBuffer, inBlockBuffer, bytesRead); } - } - }while(wrote < toDownload.getFileLength() || lastPart); - + inBlockBuffer += bytesRead; + } + + if (inBlockBuffer == blockSize || end) + { + if (first) + { + first = false; + } else + { + IV = null; + } + + byte[] output = decryptFileChunk(blockBuffer, blockSize, IV); + + if ((wrote + inBlockBuffer) > toDownload.getFileLength()) + { + inBlockBuffer = (int) (toDownload.getFileLength() - wrote); + } + + wrote += inBlockBuffer; + outputStream.write(output, 0, inBlockBuffer); + + inBlockBuffer = 0; + } + + } while (totalDownloadSize < dlFileLength && !end); + outputStream.close(); inputSteam.close(); - - } - - public void decrypt(FEntry fileEntry,String outputPath) throws IOException { - String [] path = fileEntry.getFullPath().split("/"); - boolean decryptWithHash = false; - if(!path[1].equals("code") && fileEntry.isExtractWithHash()){ - decryptWithHash = true; - } - - long fileOffset = fileEntry.getFileOffset(); - if(decryptWithHash){ - int BLOCKSIZE = 0x10000; - int HASHBLOCKSIZE = 0xFC00; - fileOffset = ((fileEntry.getFileOffset() / HASHBLOCKSIZE) * BLOCKSIZE); - } - - - InputStream input = new FileInputStream(fileEntry.getContentPath()); - FileOutputStream outputStream = new FileOutputStream(outputPath + "/" + fileEntry.getFileName()); - - input.skip(fileOffset); - - if(!decryptWithHash){ - decryptFile(input, outputStream, fileEntry); - }else{ - decryptFileHash(input, outputStream, fileEntry); - } - } - - - - -} + public void decryptFileHash(InputStream inputSteam, OutputStream outputStream, FEntry toDownload) throws IOException + { + int blockSize = 0x10000; + int hashBlockSize = 0xFC00; + long writeSize = hashBlockSize; // Hash block size + + long block = (toDownload.getFileOffset() / hashBlockSize) & 0xF; + + long sOffset = toDownload.getFileOffset() - (toDownload.getFileOffset() / hashBlockSize * hashBlockSize); + + long size = toDownload.getFileLength(); + + if (sOffset + size > writeSize) + { + writeSize = writeSize - sOffset; + } + + int bytesRead; + byte[] downloadBuffer; + + byte[] encryptedBlockBuffer = new byte[blockSize]; + byte[] buffer = new byte[blockSize]; + + int encryptedBytesInBuffer = 0; + int bufferPosition = 0; + + byte[] tmp = new byte[blockSize]; + boolean lastPart = false; + long wrote = 0; + + do + { + downloadBuffer = new byte[blockSize - bufferPosition]; + bytesRead = inputSteam.read(downloadBuffer); + int bytesInBuffer = bytesRead + bufferPosition; + if (bytesRead == -1) + { + lastPart = true; + } else + { + System.arraycopy(downloadBuffer, 0, buffer, bufferPosition, bytesRead); //copy downloaded stuff in buffer + bufferPosition = 0; + } + + if (encryptedBytesInBuffer + bytesInBuffer > blockSize) + { + int tooMuch = (encryptedBytesInBuffer + bytesInBuffer) - blockSize; + int toRead = blockSize - encryptedBytesInBuffer; + + System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, toRead); // make buffer with encrypteddata full + encryptedBytesInBuffer += toRead; + + bufferPosition = tooMuch; //set buffer position; + System.arraycopy(buffer, toRead, tmp, 0, tooMuch); + System.arraycopy(tmp, 0, buffer, 0, tooMuch); + + } else + { + if (!lastPart) + { + System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, bytesInBuffer); //When File if at the end, no more need to copy + } + encryptedBytesInBuffer += bytesInBuffer; + } + + //If downloaded block size, or file at the end: Decrypt! + if (encryptedBytesInBuffer == blockSize || lastPart) + { + if (writeSize > size) + { + writeSize = size; + } + + byte[] output = decryptFileChunkHash(encryptedBlockBuffer, blockSize, (int) block, toDownload.getContentID()); + + if ((wrote + writeSize) > toDownload.getFileLength()) + { + writeSize = (int) (toDownload.getFileLength() - wrote); + } + + outputStream.write(output, (int) (sOffset), (int) writeSize); + wrote += writeSize; + encryptedBytesInBuffer = 0; + + block++; + if (block >= 16) + { + block = 0; + } + + if (sOffset > 0) + { + writeSize = hashBlockSize; + sOffset = 0; + } + } + } while (wrote < toDownload.getFileLength() || lastPart); + + outputStream.close(); + inputSteam.close(); + } + + public void decrypt(FEntry fileEntry, String outputPath) throws IOException + { + String[] path = fileEntry.getFullPath().split("/"); + boolean decryptWithHash = false; + if (!path[1].equals("code") && fileEntry.isExtractWithHash()) + { + decryptWithHash = true; + } + + long fileOffset = fileEntry.getFileOffset(); + if (decryptWithHash) + { + int blockSize = 0x10000; + int hashBlockSize = 0xFC00; + fileOffset = ((fileEntry.getFileOffset() / hashBlockSize) * blockSize); + } + + InputStream input = new FileInputStream(fileEntry.getContentPath()); + FileOutputStream outputStream = new FileOutputStream(outputPath + "/" + fileEntry.getFileName()); + + long actualBytesSkipped = 0; + long bytesToSkip = fileOffset; + + while (actualBytesSkipped != bytesToSkip) + { + actualBytesSkipped += input.skip(bytesToSkip - actualBytesSkipped); + } + + if (!decryptWithHash) + { + decryptFile(input, outputStream, fileEntry); + } else + { + decryptFileHash(input, outputStream, fileEntry); + } + } +} \ No newline at end of file diff --git a/src/de/mas/jnustool/util/Downloader.java b/src/de/mas/jnustool/util/Downloader.java index 8c2e503..6ab0594 100644 --- a/src/de/mas/jnustool/util/Downloader.java +++ b/src/de/mas/jnustool/util/Downloader.java @@ -1,156 +1,169 @@ package de.mas.jnustool.util; +import de.mas.jnustool.FEntry; + import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; -import de.mas.jnustool.FEntry; - -public class Downloader { +public class Downloader +{ private static Downloader instance; - - public static Downloader getInstance(){ - if(instance == null){ + + public static Downloader getInstance() + { + if (instance == null) + { instance = new Downloader(); } - return instance; + + return instance; } - private Downloader(){ - + + private Downloader() + { + } - - - public void downloadAndDecrypt(FEntry toDownload) throws IOException{ - String URL = URL_BASE + "/" + String.format("%016X", toDownload.getTitleID()) + "/" + String.format("%08X", toDownload.getNUScontentID()); + + public void downloadAndDecrypt(FEntry toDownload) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", toDownload.getTitleID()) + "/" + String.format("%08X", toDownload.getNUSContentID()); URL url = new URL(URL); - String [] path = toDownload.getFullPath().split("/"); + String[] path = toDownload.getFullPath().split("/"); boolean decryptWithHash = false; - if(!path[1].equals("code") && toDownload.isExtractWithHash()){ + if (!path[1].equals("code") && toDownload.isExtractWithHash()) + { decryptWithHash = true; } - HttpURLConnection connection =(HttpURLConnection) url.openConnection(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); long fileOffset = toDownload.getFileOffset(); - - if(decryptWithHash){ - int BLOCKSIZE = 0x10000; - int HASHBLOCKSIZE = 0xFC00; - fileOffset = ((toDownload.getFileOffset() / HASHBLOCKSIZE) * BLOCKSIZE); - + + if (decryptWithHash) + { + int blockSize = 0x10000; + int hashBlockSize = 0xFC00; + fileOffset = ((toDownload.getFileOffset() / hashBlockSize) * blockSize); } - connection.setRequestProperty("Range", "bytes=" + fileOffset +"-"); - - connection.connect(); - - Decryption decryption = new Decryption(toDownload.getTicket()); - - InputStream input = connection.getInputStream(); - FileOutputStream outputStream = new FileOutputStream(String.format("%016X", toDownload.getTitleID()) +"/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length())); - if(!decryptWithHash){ - decryption.decryptFile(input, outputStream, toDownload); - }else{ - decryption.decryptFileHash(input, outputStream, toDownload); - } + connection.setRequestProperty("Range", "bytes=" + fileOffset + "-"); + + connection.connect(); + + Decryption decryption = new Decryption(toDownload.getTicket()); + + InputStream input = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(String.format("%016X", toDownload.getTitleID()) + "/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length())); + if (!decryptWithHash) + { + decryption.decryptFile(input, outputStream, toDownload); + } else + { + decryption.decryptFileHash(input, outputStream, toDownload); + } + + connection.disconnect(); + } - connection.disconnect(); - } - public static String URL_BASE = ""; - public void downloadTMD(long titleID,int version,String path) throws IOException { - downloadTMD(titleID,path); + public void downloadTMD(long titleID, String path) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd"; + downloadFile(URL, "tmd", path); } - public void downloadTMD(long titleID,String path) throws IOException { - String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd"; - downloadFile(URL, "tmd",path); - } - public void downloadFile(String fileURL,String filename,String tmpPath) throws IOException{ + + public void downloadFile(String fileURL, String filename, String tmpPath) throws IOException + { int BUFFER_SIZE = 0x800; URL url = new URL(fileURL); - HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); - - InputStream inputStream = httpConn.getInputStream(); - if(tmpPath != null){ - filename = tmpPath + "/" + filename; - } - - FileOutputStream outputStream = new FileOutputStream(filename); + HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); - int bytesRead = -1; - byte[] buffer = new byte[BUFFER_SIZE]; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } + InputStream inputStream = httpConn.getInputStream(); + if (tmpPath != null) + { + filename = tmpPath + "/" + filename; + } - outputStream.close(); - inputStream.close(); - - httpConn.disconnect(); + FileOutputStream outputStream = new FileOutputStream(filename); + + int bytesRead; + byte[] buffer = new byte[BUFFER_SIZE]; + while ((bytesRead = inputStream.read(buffer)) != -1) + { + outputStream.write(buffer, 0, bytesRead); + } + + outputStream.close(); + inputStream.close(); + + httpConn.disconnect(); } - - public void downloadFile(String fileURL,String filename) throws IOException{ - downloadFile(fileURL, filename,null); + + public void downloadTicket(long titleID, String path) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk"; + downloadFile(URL, "cetk", path); } - public void downloadTicket(long titleID,String path) throws IOException { - String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk"; - downloadFile(URL, "cetk",path); - } - public void downloadContent(long titleID,int contentID) throws IOException { - downloadContent(titleID,contentID, null); - } - 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) throws IOException { - String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd"; + + public byte[] downloadContentToByteArray(long titleID, int contentID) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID); return downloadFileToByteArray(URL); } - private byte[] downloadFileToByteArray(String fileURL) throws IOException { - + + public byte[] downloadTMDToByteArray(long titleID) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd"; + return downloadFileToByteArray(URL); + } + + private byte[] downloadFileToByteArray(String fileURL) throws IOException + { int BUFFER_SIZE = 0x800; URL url = new URL(fileURL); - HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); - int responseCode = httpConn.getResponseCode(); - - // always check HTTP response code first - byte[] file = null; - - if (responseCode == HttpURLConnection.HTTP_OK) { - int contentLength = httpConn.getContentLength(); - - file = new byte[contentLength]; - // always check HTTP response code first - - InputStream inputStream = httpConn.getInputStream(); - - int bytesRead = -1; - byte[] buffer = new byte[BUFFER_SIZE]; - int filePostion = 0; - while ((bytesRead = inputStream.read(buffer)) != -1) { - System.arraycopy(buffer, 0, file, filePostion,bytesRead); - filePostion+=bytesRead; - - } - inputStream.close(); - }else{ - System.err.println("File not found: " + fileURL); - } - httpConn.disconnect(); - return file; - + HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); + int responseCode = httpConn.getResponseCode(); + + // always check HTTP response code first + byte[] file = null; + + if (responseCode == HttpURLConnection.HTTP_OK) + { + int contentLength = httpConn.getContentLength(); + + file = new byte[contentLength]; + // always check HTTP response code first + + InputStream inputStream = httpConn.getInputStream(); + + int bytesRead; + byte[] buffer = new byte[BUFFER_SIZE]; + int filePosition = 0; + while ((bytesRead = inputStream.read(buffer)) != -1) + { + System.arraycopy(buffer, 0, file, filePosition, bytesRead); + filePosition += bytesRead; + } + inputStream.close(); + } else + { + System.err.println("File not found: " + fileURL); + } + httpConn.disconnect(); + return file; } - public byte[] downloadTicketToByteArray(long titleID) throws IOException { - String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk"; + + public byte[] downloadTicketToByteArray(long titleID) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk"; return downloadFileToByteArray(URL); } - public void downloadContent(long titleID,int contentID, String tmpPath) throws IOException { - String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID); - downloadFile(URL, String.format("%08X", contentID) +".app",tmpPath); - + + public void downloadContent(long titleID, int contentID, String tmpPath) throws IOException + { + String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID); + downloadFile(URL, String.format("%08X", contentID) + ".app", tmpPath); } - - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/util/ExitException.java b/src/de/mas/jnustool/util/ExitException.java index 54eae42..e66db24 100644 --- a/src/de/mas/jnustool/util/ExitException.java +++ b/src/de/mas/jnustool/util/ExitException.java @@ -1,14 +1,9 @@ package de.mas.jnustool.util; -public class ExitException extends Exception { - - public ExitException(String string) { +public class ExitException extends Exception +{ + public ExitException(String string) + { super(string); } - - /** - * - */ - private static final long serialVersionUID = 1L; - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/util/Settings.java b/src/de/mas/jnustool/util/Settings.java index c2f1fdf..a2c6e71 100644 --- a/src/de/mas/jnustool/util/Settings.java +++ b/src/de/mas/jnustool/util/Settings.java @@ -1,12 +1,11 @@ package de.mas.jnustool.util; - -public class Settings { +public class Settings +{ public static boolean downloadContent = false; public static boolean useCachedFiles = false; public static boolean downloadWhenCachedFilesMissingOrBroken = true; public static boolean skipBrokenFiles = false; public static boolean skipExistingFiles = true; public static boolean skipExistingTMDTICKET = true; - -} +} \ No newline at end of file diff --git a/src/de/mas/jnustool/util/Util.java b/src/de/mas/jnustool/util/Util.java deleted file mode 100644 index f4a366f..0000000 --- a/src/de/mas/jnustool/util/Util.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.mas.jnustool.util; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.Arrays; - -public class Util { - - public static byte[] commonKey; - - public static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } - - - public static String ByteArrayToString(byte[] ba) - { - StringBuilder hex = new StringBuilder(ba.length * 2); - for(byte b : ba){ - hex.append(String.format("%X", b)); - } - return hex.toString(); - } - - public static int getIntFromBytes(byte[] input,int offset){ - return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt(); - } - public static long getIntAsLongFromBytes(byte[] input,int offset){ - long result = 0 ; - if((int)input[offset]+128 > 0 && (int)input[offset]+128 < 128){ - - input[offset] += 128; - - result = (long)ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt(); - - result += 1024L*1024L*2048L; - return result; - - } - return (long)ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt(); - } - - public static short getShortFromBytes(byte[] input, int offset) { - return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+2)).getShort(); - } - - public static long StringToLong(String s) { - try{ - BigInteger bi = new BigInteger(s, 16); - return bi.longValue(); - }catch(NumberFormatException e){ - System.err.println("Invalid Title ID"); - return 0L; - } - } -}