diff --git a/src/Directory.java b/src/Directory.java new file mode 100644 index 0000000..e4d98a4 --- /dev/null +++ b/src/Directory.java @@ -0,0 +1,92 @@ +import java.util.Collection; +import java.util.TreeMap; + +import javax.swing.tree.DefaultMutableTreeNode; + +public class Directory { + String name = ""; + TreeMap folder = new TreeMap<>(); + TreeMap files = new TreeMap<>(); + + public Directory get(String s){ + return folder.get(s); + } + + public Directory(String name){ + setName(name); + } + + public boolean containsFolder(String s){ + return folder.containsKey(s); + } + + public Directory getFolder(String s){ + return folder.get(s); + } + + public Directory addFolder(Directory s){ + return folder.put(s.getName(),s); + } + public boolean containsFile(String s){ + return files.containsKey(s); + } + + public FEntry getFile(String s){ + return files.get(s); + } + + public FEntry addFile(FEntry s){ + return files.put(s.getFileName(),s); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + + + public Collection getFolder() { + return folder.values(); + } + + + + public Collection getFiles() { + return files.values(); + } + + public void setFiles(TreeMap files) { + this.files = files; + } + + @Override + 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); + } + return ""; + } + + public DefaultMutableTreeNode getNodes(){ + DefaultMutableTreeNode node = new DefaultMutableTreeNode(getName()); + + for(Directory f: getFolder()){ + node.add(f.getNodes()); + } + + for(FEntry f: getFiles()){ + node.add(new DefaultMutableTreeNode(f)); + } + return node; + } + + +} diff --git a/src/Downloader.java b/src/Downloader.java index 3a3d6c2..a3cab23 100644 --- a/src/Downloader.java +++ b/src/Downloader.java @@ -29,8 +29,7 @@ public class Downloader { if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){ dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE; } - - fileOffset = ((fileOffset / BLOCKSIZE) * BLOCKSIZE); + connection.setRequestProperty("Range", "bytes=" + fileOffset+"-"); connection.connect(); @@ -235,17 +234,21 @@ public class Downloader { String [] path = toDownload.getFullPath().split("/"); String folder = String.format("%016X", titleID) +"/"; + File folder_ = null; for(int i = 0;i pathList; public FEntry(String path, String filename, int contentID,int NUScontentID, long fileOffset, long fileLength, boolean dir, - boolean in_nus_title, boolean extract_withHash) { + boolean in_nus_title, boolean extract_withHash, List pathList) { setPath(path); setFileName(filename); setContentID(contentID); @@ -29,7 +31,7 @@ public class FEntry { setInNusTitle(in_nus_title); setExtractWithHash(extract_withHash); setNUScontentID(NUScontentID); - + setPathList(pathList); } public boolean isDir() { @@ -117,6 +119,17 @@ public class FEntry { Downloader.getInstance().download(this); } + + public List getPathList() { + return pathList; + } + + public void setPathList(List pathList) { + this.pathList = pathList; + } + + + } diff --git a/src/FST.java b/src/FST.java index c26495a..b1e99bb 100644 --- a/src/FST.java +++ b/src/FST.java @@ -14,11 +14,37 @@ public class FST { int totalEntries = 0; int dirEntries = 0; + private Directory directory = new Directory("root"); + public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException { parse(decrypteddata,tmd); + buildDirectory(); + } + + private void buildDirectory() { + for(FEntry f : getFileEntries()){ + Directory current = directory; + int i = 0; + for(String s :f.getPathList()){ + + if(current.containsFolder(s)){ + current = current.get(s); + }else{ + Directory newDir = new Directory(s); + current.addFolder(newDir); + current = newDir; + } + i++; + 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})){ @@ -99,7 +125,7 @@ public class FST { this.totalContentSize += fileLength; if(in_nus_title)this.totalContentSizeInNUS += fileLength; - + List pathList = new ArrayList<>(); //getting the full path of entry if(dir) { @@ -114,20 +140,28 @@ public class FST { StringBuilder sb = new StringBuilder(); int k = 0; int nameoffoff,nameoff_entrypath; + for( j=0; j 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 { + public void checkStateChanged(CheckChangeEvent event); + } + + public void addCheckChangeEventListener(CheckChangeEventListener listener) { + listenerList.add(CheckChangeEventListener.class, listener); + } + public void removeCheckChangeEventListener(CheckChangeEventListener listener) { + listenerList.remove(CheckChangeEventListener.class, listener); + } + + 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()]); + } + + // 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; + } + + 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(); + + + + + + + + /* + parent.add(new DefaultMutableTreeNode("hot dogs")); + parent.add(new DefaultMutableTreeNode("pizza")); + parent.add(new DefaultMutableTreeNode("ravioli")); + parent.add(new DefaultMutableTreeNode("bananas"));*/ + //return ; + + setModel(new DefaultTreeModel(nus.getFst().getDirectory().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); + } + } + +} \ No newline at end of file diff --git a/src/NUSGUI.java b/src/NUSGUI.java new file mode 100644 index 0000000..d579561 --- /dev/null +++ b/src/NUSGUI.java @@ -0,0 +1,70 @@ +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; + +public class NUSGUI extends JFrame { + + private static final long serialVersionUID = 4648172894076113183L; + + public NUSGUI(NUSTitle nus) { + 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 TitleDownloader(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/NUSTitle.java b/src/NUSTitle.java index ccb4a84..496391e 100644 --- a/src/NUSTitle.java +++ b/src/NUSTitle.java @@ -1,14 +1,10 @@ import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.ForkJoinPool; public class NUSTitle { private TitleMetaData tmd; private long titleID; private TIK ticket; - + private FST fst; public NUSTitle(long titleId,String key){ try { @@ -17,7 +13,7 @@ public class NUSTitle { Decryption decryption = new Decryption(Util.commonKey,titleId); tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray()); - + if(key == null){ ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(),tmd.titleID); }else{ @@ -31,33 +27,13 @@ public class NUSTitle { byte[] encryptedFST = Downloader.getInstance().downloadContentToByteArray(tmd.contents[0].ID); byte[] decryptedFST = decryption.decrypt(encryptedFST); - FST fst = new FST(decryptedFST,tmd); - - long start = new Date().getTime(); + fst = new FST(decryptedFST,tmd); 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()); - System.out.println(""); - System.out.println("Downloading all files."); - System.out.println(""); - - ForkJoinPool pool = ForkJoinPool.commonPool(); - List list = new ArrayList<>(); - for(FEntry f: fst.getFileEntries()){ - if(!f.isDir() && f.isInNUSTitle()) - list.add(new TitleDownloader(f)); - } - pool.invokeAll(list); - - long runningTime = new Date().getTime() - start; - System.out.println(""); - System.out.println("Done in: " + runningTime/1000.0 + " seconds"); - - - } catch (IOException e) { // TODO Auto-generated catch block @@ -65,6 +41,17 @@ public class NUSTitle { } } + + public FST getFst() { + return fst; + } + + + public void setFst(FST fst) { + this.fst = fst; + } + + public long getTitleID() { return titleID; } diff --git a/src/Starter.java b/src/Starter.java index af83eb6..2b2c325 100644 --- a/src/Starter.java +++ b/src/Starter.java @@ -6,6 +6,8 @@ import java.io.IOException; public class Starter { public static void main(String[] args) { + System.out.println("JNUSTool 0.0.1 - pre alpha - by Maschell"); + System.out.println(); try { readConfig(); } catch (IOException e) { @@ -20,14 +22,15 @@ public class Starter { if( args.length > 1 && args[1].length() == 32){ key = args[1].substring(0, 32); } - new NUSTitle(titleID, key); + NUSGUI m = new NUSGUI(new NUSTitle(titleID, key)); + m.setVisible(true); }else{ System.out.println("Need parameters: TITLEID [KEY]"); } } - private static void readConfig() throws IOException { + 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());