v0.2-development intermediate results

This commit is contained in:
Dmitry Isaenko 2019-02-15 05:44:39 +03:00
parent 3f9add019a
commit 0d9261b62c
13 changed files with 867 additions and 74 deletions

View file

@ -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;

View file

@ -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<String> 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<String> 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<File> 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();
}

View file

@ -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; }
}

View file

@ -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<String> 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<Integer, Long> ncaNameOffsets = new LinkedHashMap<>();
int offset;
long nca_offset;
long nca_size;
long nca_name_offset;
for (int i=0; i<filesCount; i++){
if (randAccessFile.read(ncaInfoArr) == 24) {
printLog("Read NCA inside NSP: " + i, MsgType.PASS);
//hexDumpUTF8(ncaInfoArr); // TODO: DEBUG
}
else {
printLog("Read NCA inside NSP: "+i, MsgType.FAIL);
randAccessFile.close();
return false;
}
offset = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 0, 4)).order(ByteOrder.LITTLE_ENDIAN).getInt();
nca_offset = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 4, 12)).order(ByteOrder.LITTLE_ENDIAN).getLong();
nca_size = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 12, 20)).order(ByteOrder.LITTLE_ENDIAN).getLong();
nca_name_offset = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 20, 24)).order(ByteOrder.LITTLE_ENDIAN).getInt(); // yes, cast from int to long.
if (offset == 0) // TODO: add consitancy of class checker or reuse with ternary operator
printLog(" Padding check", MsgType.PASS);
else
printLog(" Padding check", MsgType.WARNING);
if (nca_offset >= 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<Byte> ncaFN; // Temporary
byte[] b = new byte[1]; // Temporary
for (int i=0; i<filesCount; i++){
ncaFN = new ArrayList<>();
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();
}
}
}

View file

@ -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");
}
}

View file

@ -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);

View file

@ -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<Void> {
private final int DEFAULT_INTERFACE = 0;
@ -29,6 +34,8 @@ class UsbCommunications extends Task<Void> {
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<Void> {
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<File> nspList){
UsbCommunications(TextArea logArea, ProgressBar progressBar, List<File> 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<Void> {
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<Void> {
}
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<Void> {
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<Void> {
}
}
bufferedInStream.close();
} catch (FileNotFoundException fnfe){
printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL);
return false;
@ -596,6 +612,203 @@ class UsbCommunications extends Task<Void> {
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<PFSProvider> 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<Void> {
}
}
/**
* 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");
}
*/
}