From 0d9261b62c7af781730e33807e9123fba2fb1683 Mon Sep 17 00:00:00 2001 From: Dmitry Isaenko Date: Fri, 15 Feb 2019 05:44:39 +0300 Subject: [PATCH 1/7] v0.2-development intermediate results --- README.md | 12 +- src/main/java/nsusbloader/NSLMain.java | 2 +- .../java/nsusbloader/NSLMainController.java | 55 ++- src/main/java/nsusbloader/PFS/NCAFile.java | 25 ++ .../java/nsusbloader/PFS/PFSProvider.java | 263 ++++++++++++++ src/main/java/nsusbloader/RainbowHexDump.java | 31 ++ src/main/java/nsusbloader/ServiceWindow.java | 4 +- .../java/nsusbloader/UsbCommunications.java | 320 ++++++++++++++---- src/main/resources/NSLMain.fxml | 27 +- src/main/resources/locale_en.properties | 3 + src/main/resources/locale_ru.properties | 4 + src/main/resources/res/app.css | 50 ++- src/main/resources/res/app_light.css | 145 ++++++++ 13 files changed, 867 insertions(+), 74 deletions(-) create mode 100644 src/main/java/nsusbloader/PFS/NCAFile.java create mode 100644 src/main/java/nsusbloader/PFS/PFSProvider.java create mode 100644 src/main/java/nsusbloader/RainbowHexDump.java create mode 100644 src/main/resources/res/app_light.css diff --git a/README.md b/README.md index 13e2a20..6ea39a4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # NS-USBloader -NS-USBloader is a PC-side tinfoil NSP USB uploader. Replacement for default usb_install_pc.py +NS-USBloader is a PC-side TinFoil/GoldLeaf NSP USB uploader. Replacement for default usb_install_pc.py and GoldTree. With GUI and cookies. Read more: https://developersu.blogspot.com/2019/02/ns-usbloader-en.html -Here is the version of 'not perfect byt anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR). +Here is the version of 'not perfect but anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR). +Ok, I'm almost sure that this version has bugs. I don't remember where I downloaded it. But it works for me somehow.. +Let's rephrase, if you have working version of TinFoil DO NOT use this one. ## License @@ -59,4 +61,8 @@ Set 'Security & Privacy' if needed. ## TODO: - [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA). - [x] Windows support -- [ ] code refactoring \ No newline at end of file +- [ ] code refactoring +- [ ] GoldLeaf support +- [ ] XCI support +- [ ] Settings +- [ ] File order sort (non-critical) \ No newline at end of file diff --git a/src/main/java/nsusbloader/NSLMain.java b/src/main/java/nsusbloader/NSLMain.java index d34fd56..68e4beb 100644 --- a/src/main/java/nsusbloader/NSLMain.java +++ b/src/main/java/nsusbloader/NSLMain.java @@ -19,7 +19,7 @@ import java.util.Locale; import java.util.ResourceBundle; public class NSLMain extends Application { - static final String appVersion = "v0.1"; + static final String appVersion = "v0.2-DEVELOPMENT"; @Override public void start(Stage primaryStage) throws Exception{ ResourceBundle rb; diff --git a/src/main/java/nsusbloader/NSLMainController.java b/src/main/java/nsusbloader/NSLMainController.java index ae5febd..00578a8 100644 --- a/src/main/java/nsusbloader/NSLMainController.java +++ b/src/main/java/nsusbloader/NSLMainController.java @@ -1,10 +1,14 @@ package nsusbloader; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; +import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.FileChooser; @@ -28,9 +32,19 @@ public class NSLMainController implements Initializable { private Region btnUpStopImage; @FXML private ProgressBar progressBar; + @FXML + private ChoiceBox choiceProtocol; + @FXML + private Button switchThemeBtn; + private Region btnSwitchImage; + + @FXML + private Pane specialPane; private Thread usbThread; + private String previouslyOpenedPath; + @Override public void initialize(URL url, ResourceBundle rb) { this.resourceBundle = rb; @@ -43,6 +57,8 @@ public class NSLMainController implements Initializable { MediatorControl.getInstance().registerController(this); + specialPane.getStyleClass().add("special-pane-as-border"); // UI hacks + uploadStopBtn.setDisable(true); selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); }); uploadStopBtn.setOnAction(e->{ uploadBtnAction(); }); @@ -52,6 +68,29 @@ public class NSLMainController implements Initializable { //uploadStopBtn.getStyleClass().remove("button"); uploadStopBtn.getStyleClass().add("buttonUp"); uploadStopBtn.setGraphic(btnUpStopImage); + + ObservableList choiceProtocolList = FXCollections.observableArrayList(); + choiceProtocolList.setAll("TinFoil", "GoldLeaf"); + choiceProtocol.setItems(choiceProtocolList); + choiceProtocol.getSelectionModel().select(0); // TODO: shared settings + + this.previouslyOpenedPath = null; + + this.btnSwitchImage = new Region(); + btnSwitchImage.getStyleClass().add("regionLamp"); + switchThemeBtn.setGraphic(btnSwitchImage); + this.switchThemeBtn.setOnAction(e->switchTheme()); + } + + private void switchTheme(){ + if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app.css")) { + switchThemeBtn.getScene().getStylesheets().remove("/res/app.css"); + switchThemeBtn.getScene().getStylesheets().add("/res/app_light.css"); + } + else { + switchThemeBtn.getScene().getStylesheets().add("/res/app.css"); + switchThemeBtn.getScene().getStylesheets().remove("/res/app_light.css"); + } } /** * Functionality for selecting NSP button. @@ -61,12 +100,22 @@ public class NSLMainController implements Initializable { List filesList; FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(resourceBundle.getString("btnFileOpen")); - fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs + if (previouslyOpenedPath == null) + fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs + else { + File validator = new File(previouslyOpenedPath); + if (validator.exists()) + fileChooser.setInitialDirectory(validator); // TODO: read from prefs + else + fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs + } fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp")); filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow()); - if (filesList != null && !filesList.isEmpty()) + if (filesList != null && !filesList.isEmpty()) { setReady(filesList); + previouslyOpenedPath = filesList.get(0).getParent(); + } else setNotReady(resourceBundle.getString("logsNoFolderFileSelected")); } @@ -87,7 +136,7 @@ public class NSLMainController implements Initializable { * */ private void uploadBtnAction(){ if (usbThread == null || !usbThread.isAlive()){ - UsbCommunications usbCommunications = new UsbCommunications(logArea, progressBar, nspToUpload); //todo: progress bar + UsbCommunications usbCommunications = new UsbCommunications(logArea, progressBar, nspToUpload, choiceProtocol.getSelectionModel().getSelectedItem()); usbThread = new Thread(usbCommunications); usbThread.start(); } diff --git a/src/main/java/nsusbloader/PFS/NCAFile.java b/src/main/java/nsusbloader/PFS/NCAFile.java new file mode 100644 index 0000000..ea537ce --- /dev/null +++ b/src/main/java/nsusbloader/PFS/NCAFile.java @@ -0,0 +1,25 @@ +package nsusbloader.PFS; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Data class to hold NCA, tik, xml etc. meta-information + * */ +public class NCAFile { + //private int ncaNumber; + private byte[] ncaFileName; + private long ncaOffset; + private long ncaSize; + + //public void setNcaNumber(int ncaNumber){ this.ncaNumber = ncaNumber; } + public void setNcaFileName(byte[] ncaFileName) { this.ncaFileName = ncaFileName; } + public void setNcaOffset(long ncaOffset) { this.ncaOffset = ncaOffset; } + public void setNcaSize(long ncaSize) { this.ncaSize = ncaSize; } + + //public int getNcaNumber() {return this.ncaNumber; } + public byte[] getNcaFileName() { return ncaFileName; } + public byte[] getNcaFileNameLength() { return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFileName.length).array(); } + public long getNcaOffset() { return ncaOffset; } + public long getNcaSize() { return ncaSize; } +} diff --git a/src/main/java/nsusbloader/PFS/PFSProvider.java b/src/main/java/nsusbloader/PFS/PFSProvider.java new file mode 100644 index 0000000..ed6f55a --- /dev/null +++ b/src/main/java/nsusbloader/PFS/PFSProvider.java @@ -0,0 +1,263 @@ +package nsusbloader.PFS; + +import nsusbloader.ServiceWindow; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.BlockingQueue; + +import static nsusbloader.RainbowHexDump.hexDumpUTF8; + +/** + * Used in GoldLeaf USB protocol + * */ +public class PFSProvider { + private static final byte[] PFS0 = new byte[]{(byte)0x50, (byte)0x46, (byte)0x53, (byte)0x30}; // PFS0, and what did you think? + + private BlockingQueue msgQueue; + private enum MsgType {PASS, FAIL, INFO, WARNING} + private ResourceBundle rb; + + private RandomAccessFile randAccessFile; + private String nspFileName; + private NCAFile[] ncaFiles; + private long bodySize; + private int ticketID = -1; + + public PFSProvider(File nspFile, BlockingQueue msgQueue){ + this.msgQueue = msgQueue; + try { + this.randAccessFile = new RandomAccessFile(nspFile, "r"); + nspFileName = nspFile.getName(); + } + catch (FileNotFoundException fnfe){ + printLog("File not founnd: \n "+fnfe.getMessage(), MsgType.FAIL); + nspFileName = null; + } + if (Locale.getDefault().getISO3Language().equals("rus")) + rb = ResourceBundle.getBundle("locale", new Locale("ru")); + else + rb = ResourceBundle.getBundle("locale", new Locale("en")); + } + + public boolean init() { + if (nspFileName == null) + return false; + + int filesCount; + int header; + + printLog("Start NSP file analyze for ["+nspFileName+"]", MsgType.INFO); + try { + byte[] fileStartingBytes = new byte[12]; + // Read PFS0, files count, header, padding (4 zero bytes) + if (randAccessFile.read(fileStartingBytes) == 12) + printLog("Read file starting bytes", MsgType.PASS); + else { + printLog("Read file starting bytes", MsgType.FAIL); + randAccessFile.close(); + return false; + } + // Check PFS0 + if (Arrays.equals(PFS0, Arrays.copyOfRange(fileStartingBytes, 0, 4))) + printLog("Read PFS0", MsgType.PASS); + else { + printLog("Read PFS0", MsgType.WARNING); + if (!ServiceWindow.getConfirmationWindow(nspFileName+"\n"+rb.getString("windowTitleConfirmWrongPFS0"), rb.getString("windowBodyConfirmWrongPFS0"))) { + randAccessFile.close(); + return false; + } + } + // Get files count + filesCount = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 4, 8)).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (filesCount > 0 ) { + printLog("Read files count [" + filesCount + "]", MsgType.PASS); + } + else { + printLog("Read files count", MsgType.FAIL); + randAccessFile.close(); + return false; + } + // Get header + header = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 8, 12)).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (header > 0 ) + printLog("Read header ["+header+"]", MsgType.PASS); + else { + printLog("Read header ", MsgType.FAIL); + randAccessFile.close(); + return false; + } + //********************************************************************************************* + // Create NCA set + this.ncaFiles = new NCAFile[filesCount]; + // Collect files from NSP + byte[] ncaInfoArr = new byte[24]; // should be unsigned long, but.. java.. u know my pain man + + HashMap ncaNameOffsets = new LinkedHashMap<>(); + + int offset; + long nca_offset; + long nca_size; + long nca_name_offset; + + for (int i=0; i= 0) + printLog(" NCA offset check "+nca_offset, MsgType.PASS); + else + printLog(" NCA offset check "+nca_offset, MsgType.WARNING); + if (nca_size >= 0) + printLog(" NCA size check: "+nca_size, MsgType.PASS); + else + printLog(" NCA size check "+nca_size, MsgType.WARNING); + if (nca_name_offset >= 0) + printLog(" NCA name offset check "+nca_name_offset, MsgType.PASS); + else + printLog(" NCA name offset check "+nca_name_offset, MsgType.WARNING); + + + NCAFile ncaFile = new NCAFile(); + ncaFile.setNcaOffset(nca_offset); + ncaFile.setNcaSize(nca_size); + this.ncaFiles[i] = ncaFile; + + ncaNameOffsets.put(i, nca_name_offset); + } + // Final offset + byte[] bufForInt = new byte[4]; + if ((randAccessFile.read(bufForInt) == 4) && (Arrays.equals(bufForInt, new byte[4]))) + printLog("Final padding check", MsgType.PASS); + else + printLog("Final padding check", MsgType.WARNING); + //hexDumpUTF8(bufForInt); // TODO: DEBUG + + // Calculate position including header for body size offset + bodySize = randAccessFile.getFilePointer()+header; + //********************************************************************************************* + // Collect file names from NCAs + printLog("Collecting file names", MsgType.INFO); + List ncaFN; // Temporary + byte[] b = new byte[1]; // Temporary + for (int i=0; i(); + randAccessFile.seek(filesCount*24+16+ncaNameOffsets.get(i)); // Files cont * 24(bit for each meta-data) + 4 bytes goes after all of them + 12 bit what were in the beginning + while ((randAccessFile.read(b)) != -1){ + if (b[0] == 0x00) + break; + else + ncaFN.add(b[0]); + } + byte[] exchangeTempArray = new byte[ncaFN.size()]; + for (int j=0; j < ncaFN.size(); j++) + exchangeTempArray[j] = ncaFN.get(j); + // Find and store ticket (.tik) + if (new String(exchangeTempArray, StandardCharsets.UTF_8).toLowerCase().endsWith(".tik")) + this.ticketID = i; + this.ncaFiles[i].setNcaFileName(Arrays.copyOf(exchangeTempArray, exchangeTempArray.length)); + //hexDumpUTF8(exchangeTempArray); // TODO: DEBUG + } + randAccessFile.close(); + } + catch (IOException ioe){ + ioe.printStackTrace(); //TODO: INFORM + } + printLog("Finish NSP file analyze for ["+nspFileName+"]", MsgType.PASS); + + return true; + } + /** + * Return file name as byte array + * */ + public byte[] getBytesNspFileName(){ + return nspFileName.getBytes(StandardCharsets.UTF_8); + } + /** + * Return file name as String + * */ + public String getStringNspFileName(){ + return nspFileName; + } + /** + * Return file name length as byte array + * */ + public byte[] getBytesNspFileNameLength(){ + return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(getBytesNspFileName().length).array(); + } + /** + * Return NCA count inside of file as byte array + * */ + public byte[] getBytesCountOfNca(){ + return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFiles.length).array(); + } + /** + * Return NCA count inside of file as int + * */ + public int getIntCountOfNca(){ + return ncaFiles.length; + } + /** + * Return requested-by-number NCA file inside of file + * */ + public NCAFile getNca(int ncaNumber){ + return ncaFiles[ncaNumber]; + } + /** + * Return bodySize + * */ + public long getBodySize(){ + return bodySize; + } + /** + * Return special NCA file: ticket + * (sugar) + * */ + public int getNcaTicketID(){ + return ticketID; + } + /** + * This is what will print to textArea of the application. + **/ + private void printLog(String message, MsgType type){ + try { + switch (type){ + case PASS: + msgQueue.put("[ PASS ] "+message+"\n"); + break; + case FAIL: + msgQueue.put("[ FAIL ] "+message+"\n"); + break; + case INFO: + msgQueue.put("[ INFO ] "+message+"\n"); + break; + case WARNING: + msgQueue.put("[ WARN ] "+message+"\n"); + break; + default: + msgQueue.put(message); + } + }catch (InterruptedException ie){ + ie.printStackTrace(); + } + + } +} diff --git a/src/main/java/nsusbloader/RainbowHexDump.java b/src/main/java/nsusbloader/RainbowHexDump.java new file mode 100644 index 0000000..ee428b2 --- /dev/null +++ b/src/main/java/nsusbloader/RainbowHexDump.java @@ -0,0 +1,31 @@ +package nsusbloader; + +import java.nio.charset.StandardCharsets; + +/** + * Debug tool like hexdump <3 + */ +public class RainbowHexDump { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BLACK = "\u001B[30m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_BLUE = "\u001B[34m"; + private static final String ANSI_PURPLE = "\u001B[35m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_WHITE = "\u001B[37m"; + + + public static void hexDumpUTF8(byte[] byteArray){ + System.out.print(ANSI_BLUE); + for (int i=0; i < byteArray.length; i++) + System.out.print(String.format("%02d-", i%100)); + System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET); + for (byte b: byteArray) + System.out.print(String.format("%02x ", b)); + System.out.print("\t\t\t" + + new String(byteArray, StandardCharsets.UTF_8) + + "\n"); + } +} diff --git a/src/main/java/nsusbloader/ServiceWindow.java b/src/main/java/nsusbloader/ServiceWindow.java index c362148..6ae679f 100644 --- a/src/main/java/nsusbloader/ServiceWindow.java +++ b/src/main/java/nsusbloader/ServiceWindow.java @@ -11,7 +11,7 @@ public class ServiceWindow { * Create window with notification * */ /* // not used - static void getErrorNotification(String title, String body){ + public static void getErrorNotification(String title, String body){ Alert alertBox = new Alert(Alert.AlertType.ERROR); alertBox.setTitle(title); alertBox.setHeaderText(null); @@ -27,7 +27,7 @@ public class ServiceWindow { /** * Create notification window with confirm/deny * */ - static boolean getConfirmationWindow(String title, String body){ + public static boolean getConfirmationWindow(String title, String body){ Alert alertBox = new Alert(Alert.AlertType.CONFIRMATION); alertBox.setTitle(title); alertBox.setHeaderText(null); diff --git a/src/main/java/nsusbloader/UsbCommunications.java b/src/main/java/nsusbloader/UsbCommunications.java index 1992287..34c44e2 100644 --- a/src/main/java/nsusbloader/UsbCommunications.java +++ b/src/main/java/nsusbloader/UsbCommunications.java @@ -3,6 +3,7 @@ package nsusbloader; import javafx.concurrent.Task; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; +import nsusbloader.PFS.PFSProvider; import org.usb4java.*; import java.io.*; @@ -11,11 +12,15 @@ import java.nio.ByteOrder; import java.nio.IntBuffer; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static nsusbloader.RainbowHexDump.hexDumpUTF8; class UsbCommunications extends Task { private final int DEFAULT_INTERFACE = 0; @@ -29,6 +34,8 @@ class UsbCommunications extends Task { private Context contextNS; private DeviceHandle handlerNS; + + private String protocol; /* Ok, here is a story. We will pass to NS only file names, not full path. => see nspMap where 'key' is a file name. File name itself should not be greater then 512 bytes, but in real world it's limited by OS to something like 256 bytes. @@ -40,7 +47,8 @@ class UsbCommunications extends Task { Since this application let user an ability (theoretically) to choose same files in different folders, the latest selected file will be added to the list and handled correctly. I have no idea why he/she will make a decision to do that. Just in case, we're good in this point. */ - UsbCommunications(TextArea logArea, ProgressBar progressBar, List nspList){ + UsbCommunications(TextArea logArea, ProgressBar progressBar, List nspList, String protocol){ + this.protocol = protocol; this.nspMap = new HashMap<>(); for (File f: nspList) nspMap.put(f.getName(), f); @@ -258,51 +266,14 @@ class UsbCommunications extends Task { else printLog("Claim interface", MsgType.PASS); - - - // Send list of NSP files: - // Proceed "TUL0" - if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30} - printLog("Send list of files: handshake", MsgType.FAIL); - close(); - return null; + //-------------------------------------------------------------------------------------------------------------- + if (protocol.equals("TinFoil")) { + if (!sendListOfNSP()) + return null; + proceedCommands(); + } else { + new GoldLeaf(); } - else - printLog("Send list of files: handshake", MsgType.PASS); - //Collect file names - StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder - for(String nspFileName: nspMap.keySet()) - nspListNamesBuilder.append(nspFileName+'\n'); // And here we come with java string default encoding (UTF-16) - - byte[] nspListNames = nspListNamesBuilder.toString().getBytes(StandardCharsets.UTF_8); - ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format - byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me. - byte[] nspListSize = byteBuffer.array(); // TODO: rewind? not sure.. - //byteBuffer.reset(); - - // Sending NSP list - if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes... - printLog("Send list of files: send length.", MsgType.FAIL); - close(); - return null; - } else - printLog("Send list of files: send length.", MsgType.PASS); - if (!writeToUsb(new byte[8])) { // 8 zero bytes goes... - printLog("Send list of files: send padding.", MsgType.FAIL); - close(); - return null; - } - else - printLog("Send list of files: send padding.", MsgType.PASS); - if (!writeToUsb(nspListNames)) { // list of the names goes... - printLog("Send list of files: send list itself.", MsgType.FAIL); - close(); - return null; - } - else - printLog("Send list of files: send list itself.", MsgType.PASS); - - proceedCommands(); close(); printLog("\tEnd chain", MsgType.INFO); @@ -332,6 +303,50 @@ class UsbCommunications extends Task { } msgConsumer.interrupt(); } + private boolean sendListOfNSP(){ + // Send list of NSP files: + // Proceed "TUL0" + if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30} + printLog("Send list of files: handshake", MsgType.FAIL); + close(); + return false; + } + else + printLog("Send list of files: handshake", MsgType.PASS); + //Collect file names + StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder + for(String nspFileName: nspMap.keySet()) + nspListNamesBuilder.append(nspFileName+'\n'); // And here we come with java string default encoding (UTF-16) + + byte[] nspListNames = nspListNamesBuilder.toString().getBytes(StandardCharsets.UTF_8); + ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format + byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me. + byte[] nspListSize = byteBuffer.array(); // TODO: rewind? not sure.. + //byteBuffer.reset(); + + // Sending NSP list + if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes... + printLog("Send list of files: send length.", MsgType.FAIL); + close(); + return false; + } else + printLog("Send list of files: send length.", MsgType.PASS); + if (!writeToUsb(new byte[8])) { // 8 zero bytes goes... + printLog("Send list of files: send padding.", MsgType.FAIL); + close(); + return false; + } + else + printLog("Send list of files: send padding.", MsgType.PASS); + if (!writeToUsb(nspListNames)) { // list of the names goes... + printLog("Send list of files: send list itself.", MsgType.FAIL); + close(); + return false; + } + else + printLog("Send list of files: send list itself.", MsgType.PASS); + return true; + } /** * After we sent commands to NS, this chain starts * */ @@ -444,7 +459,7 @@ class UsbCommunications extends Task { else progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0); }catch (InterruptedException ie){ - getException().printStackTrace(); + getException().printStackTrace(); // TODO: Do something with this } } else { @@ -471,6 +486,7 @@ class UsbCommunications extends Task { } } + bufferedInStream.close(); } catch (FileNotFoundException fnfe){ printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL); return false; @@ -596,6 +612,203 @@ class UsbCommunications extends Task { return receivedBytes; } } + + private class GoldLeaf{ + // CMD G L U C ID 0 0 0 + private final byte[] CMD_ConnectionRequest = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x00, 0x00, 0x00, 0x00}; // Write-only command + private final byte[] CMD_NSPName = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x02, 0x00, 0x00, 0x00}; // Write-only command + private final byte[] CMD_NSPData = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x04, 0x00, 0x00, 0x00}; // Write-only command + + private final byte[] CMD_ConnectionResponse = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x01, 0x00, 0x00, 0x00}; + private final byte[] CMD_Start = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x03, 0x00, 0x00, 0x00}; + private final byte[] CMD_NSPContent = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x05, 0x00, 0x00, 0x00}; + private final byte[] CMD_NSPTicket = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x06, 0x00, 0x00, 0x00}; + private final byte[] CMD_Finish = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x07, 0x00, 0x00, 0x00}; + + GoldLeaf(){ + List pfsList = new ArrayList<>(); + + StringBuilder allValidFiles = new StringBuilder(); + StringBuilder nonValidFiles = new StringBuilder(); + // Prepare data + for (File nspFile : nspMap.values()) { + PFSProvider pfsp = new PFSProvider(nspFile, msgQueue); + if (pfsp.init()) { + pfsList.add(pfsp); + allValidFiles.append(nspFile.getName()); + allValidFiles.append("\n"); + } + else { + nonValidFiles.append(nspFile.getName()); + nonValidFiles.append("\n"); + } + } + if (pfsList.size() == 0){ + printLog("All files provided have incorrect structure and won't be uploaded", MsgType.FAIL); + return; + } + printLog("===========================================================================", MsgType.INFO); + printLog("Verified files prepared for upload: \n "+allValidFiles, MsgType.PASS); + if (!nonValidFiles.toString().isEmpty()) + printLog("Files with incorrect structure that won't be uploaded: \n"+nonValidFiles, MsgType.INFO); + //-------------------------------------------------------------------------------------------------------------- + + // Go parse commands + byte[] readByte; + + for(PFSProvider pfsElement: pfsList) { + // Go connect to GoldLeaf + if (writeToUsb(CMD_ConnectionRequest)) + printLog("Initiating GoldLeaf connection" + nonValidFiles, MsgType.PASS); + else { + printLog("Initiating GoldLeaf connection" + nonValidFiles, MsgType.FAIL); + return; + } + int a = 0; // TODO:DEBUG + while (true) { + System.out.println("In loop. Iter: "+a); // TODO:DEBUG + readByte = readFromUsb(); + if (readByte == null) + return; + hexDumpUTF8(readByte); // TODO:DEBUG + if (Arrays.equals(readByte, CMD_ConnectionResponse)) { + if (!handleConnectionResponse(pfsElement)) + return; + else + continue; + } + if (Arrays.equals(readByte, CMD_Start)) { + if (!handleStart(pfsElement)) + return; + else + continue; + } + if (Arrays.equals(readByte, CMD_NSPContent)) { + if (!handleNSPContent(pfsElement, true)) + return; + else + continue; + } + if (Arrays.equals(readByte, CMD_NSPTicket)) { + if (!handleNSPContent(pfsElement, false)) + return; + else + continue; + } + if (Arrays.equals(readByte, CMD_Finish)) { + printLog("Closing GoldLeaf connection: Transfer successful", MsgType.PASS); + break; // TODO: GO TO NEXT NSP + } + } + } + } + /** + * ConnectionResponse command handler + * */ + private boolean handleConnectionResponse(PFSProvider pfsElement){ + if (!writeToUsb(CMD_NSPName)) + return false; + if (!writeToUsb(pfsElement.getBytesNspFileNameLength())) + return false; + if (!writeToUsb(pfsElement.getBytesNspFileName())) + return false; + return true; + } + /** + * Start command handler + * */ + private boolean handleStart(PFSProvider pfsElement){ + if (!writeToUsb(CMD_NSPData)) + return false; + if (!writeToUsb(pfsElement.getBytesCountOfNca())) + return false; + for (int i = 0; i < pfsElement.getIntCountOfNca(); i++){ + if (!writeToUsb(pfsElement.getNca(i).getNcaFileNameLength())) + return false; + if (!writeToUsb(pfsElement.getNca(i).getNcaFileName())) + return false; + if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getBodySize()+pfsElement.getNca(i).getNcaOffset()).array())) // offset. real. + return false; + if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getNca(i).getNcaSize()).array())) // size + return false; + } + return true; + } + /** + * NSPContent command handler + * isItRawRequest - if True, just ask NS what's needed + * - if False, send ticket + * */ + private boolean handleNSPContent(PFSProvider pfsElement, boolean isItRawRequest){ + int requestedNcaID; + boolean isProgessBarInitiated = false; + if (isItRawRequest) { + byte[] readByte = readFromUsb(); + if (readByte == null || readByte.length != 4) + return false; + requestedNcaID = ByteBuffer.wrap(readByte).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + else { + requestedNcaID = pfsElement.getNcaTicketID(); + } + + long realNcaOffset = pfsElement.getNca(requestedNcaID).getNcaOffset()+pfsElement.getBodySize(); + long realNcaSize = pfsElement.getNca(requestedNcaID).getNcaSize(); + + long readFrom = 0; + + int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576 + byte[] readBuf; + File nspFile = nspMap.get(pfsElement.getStringNspFileName()); // wuuuut ( >< ) + try{ + BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspFile)); // TODO: refactor? + if (bufferedInStream.skip(realNcaOffset) != realNcaOffset) + return false; + + while (readFrom < realNcaSize){ + + if (Thread.currentThread().isInterrupted()) // Check if user interrupted process. + return false; + + if (realNcaSize - readFrom < readPice) + readPice = Math.toIntExact(realNcaSize - readFrom); // it's safe, I guarantee + readBuf = new byte[readPice]; + if (bufferedInStream.read(readBuf) != readPice) + return false; + + if (!writeToUsb(readBuf)) + return false; + /***********************************/ + if (isProgessBarInitiated){ + try { + if (readFrom+readPice == realNcaSize){ + progressQueue.put(1.0); + isProgessBarInitiated = false; + } + else + progressQueue.put((readFrom+readPice)/(realNcaSize/100.0) / 100.0); + }catch (InterruptedException ie){ + getException().printStackTrace(); // TODO: Do something with this + } + } + else { + if ((readPice == 8388608) && (readFrom == 0)) + isProgessBarInitiated = true; + } + /***********************************/ + readFrom += readPice; + } + bufferedInStream.close(); + } + catch (IOException ioe){ + ioe.printStackTrace(); + return false; + } + return true; + } + } + + //------------------------------------------------------------------------------------------------------------------ /** * This is what will print to textArea of the application. * */ @@ -622,19 +835,4 @@ class UsbCommunications extends Task { } } - /** - * Debug tool like hexdump <3 - */ - /* - private void hexDumpUTF8(byte[] byteArray){ - for (int i=0; i < byteArray.length; i++) - System.out.print(String.format("%02d-", i%10)); - System.out.println("\t[[COLUMNS LEN = "+byteArray.length+"]]"); - for (byte b: byteArray) - System.out.print(String.format("%02x ", b)); - System.out.print("\t\t\t" - + new String(byteArray, StandardCharsets.UTF_8) - + "\n"); - } - */ } diff --git a/src/main/resources/NSLMain.fxml b/src/main/resources/NSLMain.fxml index e3114b7..cc2efb2 100644 --- a/src/main/resources/NSLMain.fxml +++ b/src/main/resources/NSLMain.fxml @@ -2,11 +2,16 @@ + + + + + @@ -14,9 +19,29 @@ + + + + +