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

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

View file

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

View file

@ -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"));
if (previouslyOpenedPath == null)
fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs 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();
} }

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

View file

@ -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"
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; 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(); proceedCommands();
} else {
new GoldLeaf();
}
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");
}
*/
} }

View file

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

View file

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

View file

@ -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?\

View file

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

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