v0.2-development intermediate results
This commit is contained in:
parent
3f9add019a
commit
0d9261b62c
13 changed files with 867 additions and 74 deletions
10
README.md
10
README.md
|
@ -1,12 +1,14 @@
|
||||||
# NS-USBloader
|
# 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.
|
With GUI and cookies.
|
||||||
|
|
||||||
Read more: https://developersu.blogspot.com/2019/02/ns-usbloader-en.html
|
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
|
## License
|
||||||
|
|
||||||
|
@ -60,3 +62,7 @@ Set 'Security & Privacy' if needed.
|
||||||
- [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA).
|
- [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA).
|
||||||
- [x] Windows support
|
- [x] Windows support
|
||||||
- [ ] code refactoring
|
- [ ] code refactoring
|
||||||
|
- [ ] GoldLeaf support
|
||||||
|
- [ ] XCI support
|
||||||
|
- [ ] Settings
|
||||||
|
- [ ] File order sort (non-critical)
|
|
@ -19,7 +19,7 @@ import java.util.Locale;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public class NSLMain extends Application {
|
public class NSLMain extends Application {
|
||||||
static final String appVersion = "v0.1";
|
static final String appVersion = "v0.2-DEVELOPMENT";
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage primaryStage) throws Exception{
|
public void start(Stage primaryStage) throws Exception{
|
||||||
ResourceBundle rb;
|
ResourceBundle rb;
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package nsusbloader;
|
package nsusbloader;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ChoiceBox;
|
||||||
import javafx.scene.control.ProgressBar;
|
import javafx.scene.control.ProgressBar;
|
||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
|
|
||||||
|
@ -28,9 +32,19 @@ public class NSLMainController implements Initializable {
|
||||||
private Region btnUpStopImage;
|
private Region btnUpStopImage;
|
||||||
@FXML
|
@FXML
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
|
@FXML
|
||||||
|
private ChoiceBox<String> choiceProtocol;
|
||||||
|
@FXML
|
||||||
|
private Button switchThemeBtn;
|
||||||
|
private Region btnSwitchImage;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Pane specialPane;
|
||||||
|
|
||||||
private Thread usbThread;
|
private Thread usbThread;
|
||||||
|
|
||||||
|
private String previouslyOpenedPath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL url, ResourceBundle rb) {
|
public void initialize(URL url, ResourceBundle rb) {
|
||||||
this.resourceBundle = rb;
|
this.resourceBundle = rb;
|
||||||
|
@ -43,6 +57,8 @@ public class NSLMainController implements Initializable {
|
||||||
|
|
||||||
MediatorControl.getInstance().registerController(this);
|
MediatorControl.getInstance().registerController(this);
|
||||||
|
|
||||||
|
specialPane.getStyleClass().add("special-pane-as-border"); // UI hacks
|
||||||
|
|
||||||
uploadStopBtn.setDisable(true);
|
uploadStopBtn.setDisable(true);
|
||||||
selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); });
|
selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); });
|
||||||
uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
|
uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
|
||||||
|
@ -52,6 +68,29 @@ public class NSLMainController implements Initializable {
|
||||||
//uploadStopBtn.getStyleClass().remove("button");
|
//uploadStopBtn.getStyleClass().remove("button");
|
||||||
uploadStopBtn.getStyleClass().add("buttonUp");
|
uploadStopBtn.getStyleClass().add("buttonUp");
|
||||||
uploadStopBtn.setGraphic(btnUpStopImage);
|
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.
|
* Functionality for selecting NSP button.
|
||||||
|
@ -61,12 +100,22 @@ public class NSLMainController implements Initializable {
|
||||||
List<File> filesList;
|
List<File> filesList;
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle(resourceBundle.getString("btnFileOpen"));
|
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"));
|
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp"));
|
||||||
|
|
||||||
filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow());
|
filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow());
|
||||||
if (filesList != null && !filesList.isEmpty())
|
if (filesList != null && !filesList.isEmpty()) {
|
||||||
setReady(filesList);
|
setReady(filesList);
|
||||||
|
previouslyOpenedPath = filesList.get(0).getParent();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
setNotReady(resourceBundle.getString("logsNoFolderFileSelected"));
|
setNotReady(resourceBundle.getString("logsNoFolderFileSelected"));
|
||||||
}
|
}
|
||||||
|
@ -87,7 +136,7 @@ public class NSLMainController implements Initializable {
|
||||||
* */
|
* */
|
||||||
private void uploadBtnAction(){
|
private void uploadBtnAction(){
|
||||||
if (usbThread == null || !usbThread.isAlive()){
|
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 = new Thread(usbCommunications);
|
||||||
usbThread.start();
|
usbThread.start();
|
||||||
}
|
}
|
||||||
|
|
25
src/main/java/nsusbloader/PFS/NCAFile.java
Normal file
25
src/main/java/nsusbloader/PFS/NCAFile.java
Normal 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; }
|
||||||
|
}
|
263
src/main/java/nsusbloader/PFS/PFSProvider.java
Normal file
263
src/main/java/nsusbloader/PFS/PFSProvider.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
31
src/main/java/nsusbloader/RainbowHexDump.java
Normal file
31
src/main/java/nsusbloader/RainbowHexDump.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ public class ServiceWindow {
|
||||||
* Create window with notification
|
* Create window with notification
|
||||||
* */
|
* */
|
||||||
/* // not used
|
/* // not used
|
||||||
static void getErrorNotification(String title, String body){
|
public static void getErrorNotification(String title, String body){
|
||||||
Alert alertBox = new Alert(Alert.AlertType.ERROR);
|
Alert alertBox = new Alert(Alert.AlertType.ERROR);
|
||||||
alertBox.setTitle(title);
|
alertBox.setTitle(title);
|
||||||
alertBox.setHeaderText(null);
|
alertBox.setHeaderText(null);
|
||||||
|
@ -27,7 +27,7 @@ public class ServiceWindow {
|
||||||
/**
|
/**
|
||||||
* Create notification window with confirm/deny
|
* 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);
|
Alert alertBox = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
alertBox.setTitle(title);
|
alertBox.setTitle(title);
|
||||||
alertBox.setHeaderText(null);
|
alertBox.setHeaderText(null);
|
||||||
|
|
|
@ -3,6 +3,7 @@ package nsusbloader;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.scene.control.ProgressBar;
|
import javafx.scene.control.ProgressBar;
|
||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
|
import nsusbloader.PFS.PFSProvider;
|
||||||
import org.usb4java.*;
|
import org.usb4java.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
@ -11,11 +12,15 @@ import java.nio.ByteOrder;
|
||||||
import java.nio.IntBuffer;
|
import java.nio.IntBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static nsusbloader.RainbowHexDump.hexDumpUTF8;
|
||||||
|
|
||||||
class UsbCommunications extends Task<Void> {
|
class UsbCommunications extends Task<Void> {
|
||||||
private final int DEFAULT_INTERFACE = 0;
|
private final int DEFAULT_INTERFACE = 0;
|
||||||
|
@ -29,6 +34,8 @@ class UsbCommunications extends Task<Void> {
|
||||||
|
|
||||||
private Context contextNS;
|
private Context contextNS;
|
||||||
private DeviceHandle handlerNS;
|
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.
|
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.
|
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.
|
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.
|
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<>();
|
this.nspMap = new HashMap<>();
|
||||||
for (File f: nspList)
|
for (File f: nspList)
|
||||||
nspMap.put(f.getName(), f);
|
nspMap.put(f.getName(), f);
|
||||||
|
@ -258,51 +266,14 @@ class UsbCommunications extends Task<Void> {
|
||||||
else
|
else
|
||||||
printLog("Claim interface", MsgType.PASS);
|
printLog("Claim interface", MsgType.PASS);
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------------------------------------------
|
||||||
|
if (protocol.equals("TinFoil")) {
|
||||||
// Send list of NSP files:
|
if (!sendListOfNSP())
|
||||||
// Proceed "TUL0"
|
return null;
|
||||||
if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30}
|
proceedCommands();
|
||||||
printLog("Send list of files: handshake", MsgType.FAIL);
|
} else {
|
||||||
close();
|
new GoldLeaf();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
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();
|
close();
|
||||||
printLog("\tEnd chain", MsgType.INFO);
|
printLog("\tEnd chain", MsgType.INFO);
|
||||||
|
@ -332,6 +303,50 @@ class UsbCommunications extends Task<Void> {
|
||||||
}
|
}
|
||||||
msgConsumer.interrupt();
|
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
|
* After we sent commands to NS, this chain starts
|
||||||
* */
|
* */
|
||||||
|
@ -444,7 +459,7 @@ class UsbCommunications extends Task<Void> {
|
||||||
else
|
else
|
||||||
progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0);
|
progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0);
|
||||||
}catch (InterruptedException ie){
|
}catch (InterruptedException ie){
|
||||||
getException().printStackTrace();
|
getException().printStackTrace(); // TODO: Do something with this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -471,6 +486,7 @@ class UsbCommunications extends Task<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
bufferedInStream.close();
|
||||||
} catch (FileNotFoundException fnfe){
|
} catch (FileNotFoundException fnfe){
|
||||||
printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL);
|
printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL);
|
||||||
return false;
|
return false;
|
||||||
|
@ -596,6 +612,203 @@ class UsbCommunications extends Task<Void> {
|
||||||
return receivedBytes;
|
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.
|
* 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");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,16 @@
|
||||||
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ChoiceBox?>
|
||||||
<?import javafx.scene.control.ProgressBar?>
|
<?import javafx.scene.control.ProgressBar?>
|
||||||
<?import javafx.scene.control.TextArea?>
|
<?import javafx.scene.control.TextArea?>
|
||||||
|
<?import javafx.scene.control.ToolBar?>
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
<?import javafx.scene.layout.AnchorPane?>
|
||||||
|
<?import javafx.scene.layout.ColumnConstraints?>
|
||||||
|
<?import javafx.scene.layout.GridPane?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.Pane?>
|
<?import javafx.scene.layout.Pane?>
|
||||||
|
<?import javafx.scene.layout.RowConstraints?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.shape.SVGPath?>
|
<?import javafx.scene.shape.SVGPath?>
|
||||||
|
|
||||||
|
@ -14,9 +19,29 @@
|
||||||
<children>
|
<children>
|
||||||
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||||
<children>
|
<children>
|
||||||
|
<ToolBar>
|
||||||
|
<items>
|
||||||
|
<Pane HBox.hgrow="ALWAYS" />
|
||||||
|
<ChoiceBox fx:id="choiceProtocol" prefWidth="120.0" />
|
||||||
|
<Button fx:id="switchThemeBtn" mnemonicParsing="false" />
|
||||||
|
</items>
|
||||||
|
</ToolBar>
|
||||||
|
<GridPane>
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="SOMETIMES" />
|
||||||
|
<ColumnConstraints hgrow="SOMETIMES" percentWidth="90.0" />
|
||||||
|
<ColumnConstraints hgrow="SOMETIMES" />
|
||||||
|
</columnConstraints>
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints vgrow="SOMETIMES" />
|
||||||
|
</rowConstraints>
|
||||||
|
<children>
|
||||||
|
<Pane fx:id="specialPane" GridPane.columnIndex="1" />
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
<TextArea fx:id="logArea" editable="false" VBox.vgrow="ALWAYS">
|
<TextArea fx:id="logArea" editable="false" VBox.vgrow="ALWAYS">
|
||||||
<VBox.margin>
|
<VBox.margin>
|
||||||
<Insets left="5.0" right="5.0" top="5.0" />
|
<Insets bottom="2.0" left="5.0" right="5.0" top="5.0" />
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</TextArea>
|
</TextArea>
|
||||||
<ProgressBar fx:id="progressBar" prefWidth="Infinity" progress="0.0">
|
<ProgressBar fx:id="progressBar" prefWidth="Infinity" progress="0.0">
|
||||||
|
|
|
@ -12,3 +12,6 @@ logsGreetingsMessage2=--\n\
|
||||||
Source: https://github.com/developersu/ns-usbloader/\n\
|
Source: https://github.com/developersu/ns-usbloader/\n\
|
||||||
Site: https://developersu.blogspot.com/search/label/NS-USBloader\n\
|
Site: https://developersu.blogspot.com/search/label/NS-USBloader\n\
|
||||||
Dmitry Isaenko [developer.su]
|
Dmitry Isaenko [developer.su]
|
||||||
|
windowTitleConfirmWrongPFS0=Incorrect file type
|
||||||
|
windowBodyConfirmWrongPFS0=Selected NSP file has incrrect starting symbols. Most likely it's corrupted.\n\
|
||||||
|
It's better to interrupt proccess now. Continue process anyway?
|
||||||
|
|
|
@ -12,4 +12,8 @@ logsGreetingsMessage2=--\n\
|
||||||
\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0439 \u043A\u043E\u0434: https://github.com/developersu/ns-usbloader/\n\
|
\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0439 \u043A\u043E\u0434: https://github.com/developersu/ns-usbloader/\n\
|
||||||
\u0421\u0430\u0439\u0442: https://developersu.blogspot.com/search/label/NS-USBloader\n\
|
\u0421\u0430\u0439\u0442: https://developersu.blogspot.com/search/label/NS-USBloader\n\
|
||||||
\u0418\u0441\u0430\u0435\u043D\u043A\u043E \u0414\u043C\u0438\u0442\u0440\u0438\u0439 [developer.su]
|
\u0418\u0441\u0430\u0435\u043D\u043A\u043E \u0414\u043C\u0438\u0442\u0440\u0438\u0439 [developer.su]
|
||||||
|
windowTitleConfirmWrongPFS0=\u041D\u0435\u043F\u0440\u0438\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430
|
||||||
|
windowBodyConfirmWrongPFS0=\u0412\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439 \u0444\u0430\u0439\u043B NSP \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B. \u041E\u043D \u043D\u0430\u0432\u0435\u0440\u043D\u044F\u043A\u0430 \u043F\u043E\u0432\u0440\u0435\u0436\u0434\u0451\u043D.\n\
|
||||||
|
\u041B\u0443\u0447\u0448\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441. \u0425\u043E\u0447\u0435\u0448\u044C \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0438 \u0441\u043C\u043E\u0442\u0440\u044F \u043D\u0438 \u043D\u0430 \u0447\u0442\u043E?\
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,16 @@
|
||||||
-fx-border-radius: 3;
|
-fx-border-radius: 3;
|
||||||
-fx-border-width: 2;
|
-fx-border-width: 2;
|
||||||
-fx-text-fill: #f7fafa;
|
-fx-text-fill: #f7fafa;
|
||||||
|
-fx-effect: none;
|
||||||
}
|
}
|
||||||
.button:hover, .buttonStop:hover, .buttonUp:hover{
|
.button:hover, .buttonStop:hover, .buttonUp:hover, .choice-box:hover{
|
||||||
-fx-background-color: #4f4f4f;
|
-fx-background-color: #4f4f4f;
|
||||||
-fx-border-color: #a4ffff;
|
-fx-border-color: #a4ffff;
|
||||||
-fx-border-radius: 3;
|
-fx-border-radius: 3;
|
||||||
-fx-border-width: 2;
|
-fx-border-width: 2;
|
||||||
-fx-text-fill: #f7fafa;
|
-fx-text-fill: #f7fafa;
|
||||||
}
|
}
|
||||||
.button:focused, .buttonStop:focused, .buttonUp:focused{
|
.button:focused, .buttonStop:focused, .buttonUp:focused, .choice-box:focused{
|
||||||
-fx-background-color: #6a6a6a;
|
-fx-background-color: #6a6a6a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,43 @@
|
||||||
-fx-padding: 10 5 10 5;
|
-fx-padding: 10 5 10 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-bar{
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-pane-as-border{
|
||||||
|
-fx-background-color: #f7fafa;
|
||||||
|
-fx-min-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-box {
|
||||||
|
-fx-background-color: #4f4f4f;
|
||||||
|
-fx-border-color: #4f4f4f;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-mark-color: #eea11e;
|
||||||
|
-fx-effect: none;
|
||||||
|
}
|
||||||
|
.choice-box > .label {
|
||||||
|
-fx-text-fill: #f7fafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-box:pressed{
|
||||||
|
-fx-background-color: #4f4f4f;
|
||||||
|
-fx-border-color: #eea11e;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #f7fafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background color of the whole context menu
|
||||||
|
.choice-box .context-menu { -fx-background-color: #2d2d2d; }
|
||||||
|
// Focused item background color in the list
|
||||||
|
.choice-box .menu-item:focused { -fx-background-color: #eea11e; }
|
||||||
|
// Text color of non-focused items in the list
|
||||||
|
.choice-box .menu-item > .label { -fx-text-fill: #f7fafa; }
|
||||||
|
// Text color of focused item in the list
|
||||||
|
.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; }
|
||||||
|
|
||||||
.regionUpload{
|
.regionUpload{
|
||||||
-fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
|
-fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
|
||||||
|
@ -98,3 +134,11 @@
|
||||||
-fx-min-height: -size;
|
-fx-min-height: -size;
|
||||||
-fx-min-width: -size;
|
-fx-min-width: -size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.regionLamp{
|
||||||
|
-fx-shape: "M12,2A7,7 0 0,1 19,9C19,11.38 17.81,13.47 16,14.74V17A1,1 0 0,1 15,18H9A1,1 0 0,1 8,17V14.74C6.19,13.47 5,11.38 5,9A7,7 0 0,1 12,2M9,21V20H15V21A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21M12,4A5,5 0 0,0 7,9C7,11.05 8.23,12.81 10,13.58V16H14V13.58C15.77,12.81 17,11.05 17,9A5,5 0 0,0 12,4Z";
|
||||||
|
-fx-background-color: #f7fafa;
|
||||||
|
-size: 17.5;
|
||||||
|
-fx-min-height: -size;
|
||||||
|
-fx-min-width: 12.5;
|
||||||
|
}
|
145
src/main/resources/res/app_light.css
Normal file
145
src/main/resources/res/app_light.css
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
@font-face {
|
||||||
|
src: url("NotoMono-Regular.ttf");
|
||||||
|
}
|
||||||
|
.root{
|
||||||
|
-fx-background: #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button, .buttonUp, .buttonStop{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #fefefe;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
-fx-effect: dropshadow(three-pass-box, #b4b4b4, 2, 0, 0, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
.button:hover, .buttonStop:hover, .buttonUp:hover, .choice-box:hover{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #a4ffff;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
.button:focused, .buttonStop:focused, .buttonUp:focused, .choice-box:focused{
|
||||||
|
-fx-background-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:pressed{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #289de8;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
.buttonUp:pressed{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #a2e019;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
.buttonStop:pressed{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #fb582c;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area{
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-control-inner-background: #fefefe;
|
||||||
|
-fx-font-family: "Noto Mono";
|
||||||
|
-fx-border-color: #06b9bb;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-box-border: transparent;
|
||||||
|
}
|
||||||
|
.progress-bar > .track {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-box-border: transparent;
|
||||||
|
}
|
||||||
|
.progress-bar > .bar {
|
||||||
|
-fx-background-color: linear-gradient(to right, #00bce4, #ff5f53);
|
||||||
|
-fx-background-radius: 2;
|
||||||
|
-fx-background-insets: 1 1 2 1;
|
||||||
|
-fx-padding: 0.23em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-pane {
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
}
|
||||||
|
.dialog-pane > .button-bar > .container{
|
||||||
|
-fx-background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-pane > .label{
|
||||||
|
-fx-padding: 10 5 10 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-bar{
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-pane-as-border{
|
||||||
|
-fx-background-color: #2c2c2c;
|
||||||
|
-fx-min-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-box {
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #fefefe;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-mark-color: #eea11e;
|
||||||
|
-fx-effect: dropshadow(three-pass-box, #b4b4b4, 2, 0, 0, 0);
|
||||||
|
}
|
||||||
|
.choice-box > .label {
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-box:pressed{
|
||||||
|
-fx-background-color: #fefefe;
|
||||||
|
-fx-border-color: #eea11e;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-text-fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background color of the whole context menu
|
||||||
|
.choice-box .context-menu { -fx-background-color: #fefefe; }
|
||||||
|
// Focused item background color in the list
|
||||||
|
.choice-box .menu-item:focused { -fx-background-color: #eea11e; }
|
||||||
|
// Text color of non-focused items in the list
|
||||||
|
.choice-box .menu-item > .label { -fx-text-fill: #2c2c2c; }
|
||||||
|
// Text color of focused item in the list
|
||||||
|
.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; }
|
||||||
|
|
||||||
|
.regionUpload{
|
||||||
|
-fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
|
||||||
|
-fx-background-color: #a2e019;
|
||||||
|
-size: 24;
|
||||||
|
-fx-min-height: -size;
|
||||||
|
-fx-min-width: 20;
|
||||||
|
}
|
||||||
|
.regionStop{
|
||||||
|
-fx-shape: "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z";
|
||||||
|
-fx-background-color: #fb582c;
|
||||||
|
-size: 24;
|
||||||
|
-fx-min-height: -size;
|
||||||
|
-fx-min-width: -size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regionLamp {
|
||||||
|
-fx-shape: "M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z";
|
||||||
|
-fx-background-color: #2c2c2c;
|
||||||
|
-size: 17.5;
|
||||||
|
-fx-min-height: -size;
|
||||||
|
-fx-min-width: 12.5;
|
||||||
|
}
|
Loading…
Reference in a new issue