diff --git a/README.md b/README.md index 13e2a20..d2b131d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # NS-USBloader -NS-USBloader is a PC-side tinfoil NSP USB uploader. Replacement for default usb_install_pc.py +NS-USBloader is a PC-side TinFoil/GoldLeaf NSP USB uploader. Replacement for default usb_install_pc.py and GoldTree. With GUI and cookies. Read more: https://developersu.blogspot.com/2019/02/ns-usbloader-en.html -Here is the version of 'not perfect byt anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR). +Here is the version of 'not perfect but anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR). +Ok, I'm almost sure that this version has bugs. I don't remember where I downloaded it. But it works for me somehow.. +Let's rephrase, if you have working version of TinFoil DO NOT use this one. ## License @@ -32,7 +34,7 @@ Install JRE/JDK 8 or higher (openJDK is good. Oracle's one is also good). JavaFX See 'Linux' section. -Set 'Security & Privacy' if needed. +Set 'Security & Privacy' settings if needed. ### Windows: @@ -56,7 +58,22 @@ Set 'Security & Privacy' if needed. ## Known bugs * Unable to interrupt transmission when libusb awaiting for read event (when user sent NSP list but didn't selected anything on NS). +## NOTES +Table 'Status' = 'Uploaded' does not means that file installed. It means that it has been sent to NS without any issues! That's what this app about. +Handling successful/failed installation is a purpose of the other side application (TinFoil/GoldLeaf). (And they don't provide any feedback interfaces so I can't detect success/failure.) + ## TODO: -- [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA). +- [x] macOS QA v0.1 +- [ ] macOS QA v0.2 (partly) - [x] Windows support -- [ ] code refactoring \ No newline at end of file +- [ ] code refactoring (almost. todo: printLog() ) +- [x] GoldLeaf support +- [ ] XCI support +- [ ] File order sort (non-critical) + +## Thanks +Appreciate assistance and support of both Vitaliy and Konstantin. Without you all this magic would not have happened. + +[Konstanin Kelemen](https://github.com/konstantin-kelemen) + +[Vitaliy Natarov](https://github.com/SebastianUA) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2232a90..a04cb06 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,50 @@ 4.0.0 loper - NS-USBloader - 0.1-SNAPSHOT NS-USBloader + + ns-usbloader + 0.2-SNAPSHOT + + https://github.com/developersu/ns-usbloader/ + + NSP USB loader for TinFoil and GoldLeaf + + 2019 + + Dmitry Isaenko + https://developersu.blogspot.com/ + + + + + GPLv3 + LICENSE + manual + + + + + + developer.su + Dmitry Isaenko + + Developer + + +3 + https://developersu.blogspot.com/ + + + UTF-8 + + + GitHub + https://github.com/developer_su/${project.artifactId}/issues + + org.openjfx diff --git a/src/main/java/nsusbloader/AppPreferences.java b/src/main/java/nsusbloader/AppPreferences.java new file mode 100644 index 0000000..66f24f2 --- /dev/null +++ b/src/main/java/nsusbloader/AppPreferences.java @@ -0,0 +1,23 @@ +package nsusbloader; + +import java.util.prefs.Preferences; + +public class AppPreferences { + private static final AppPreferences INSTANCE = new AppPreferences(); + public static AppPreferences getInstance() { return INSTANCE; } + + private Preferences preferences; + + private AppPreferences(){ preferences = Preferences.userRoot().node("NS-USBloader"); } + + public String getTheme(){ + String theme = preferences.get("THEME", "/res/app_dark.css"); // Don't let user to change settings manually + if (!theme.matches("(^/res/app_dark.css$)|(^/res/app_light.css$)")) + theme = "/res/app_dark.css"; + return theme; + } + public void setTheme(String theme){ preferences.put("THEME", theme); } + + public String getRecent(){ return preferences.get("RECENT", System.getProperty("user.home")); } + public void setRecent(String path){ preferences.put("RECENT", path); } +} diff --git a/src/main/java/nsusbloader/Controllers/NSLMainController.java b/src/main/java/nsusbloader/Controllers/NSLMainController.java new file mode 100644 index 0000000..1ae0ef0 --- /dev/null +++ b/src/main/java/nsusbloader/Controllers/NSLMainController.java @@ -0,0 +1,195 @@ +package nsusbloader.Controllers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.stage.FileChooser; +import nsusbloader.AppPreferences; +import nsusbloader.MediatorControl; +import nsusbloader.NSLMain; +import nsusbloader.UsbCommunications; + +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.ResourceBundle; + +public class NSLMainController implements Initializable { + + private ResourceBundle resourceBundle; + + @FXML + public TextArea logArea; // Accessible from Mediator + @FXML + private Button selectNspBtn; + @FXML + private Button uploadStopBtn; + private Region btnUpStopImage; + @FXML + public ProgressBar progressBar; // Accessible from Mediator + @FXML + private ChoiceBox choiceProtocol; + @FXML + private Button switchThemeBtn; + + @FXML + private Pane specialPane; + + @FXML + public NSTableViewController tableFilesListController; // Accessible from Mediator + + private Thread usbThread; + + private String previouslyOpenedPath; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.resourceBundle = rb; + logArea.setText(rb.getString("logsGreetingsMessage")+" "+ NSLMain.appVersion+"!\n"); + if (System.getProperty("os.name").toLowerCase().startsWith("lin")) + if (!System.getProperty("user.name").equals("root")) + logArea.appendText(rb.getString("logsEnteredAsMsg1")+System.getProperty("user.name")+"\n"+rb.getString("logsEnteredAsMsg2") + "\n"); + + logArea.appendText(rb.getString("logsGreetingsMessage2")+"\n"); + + MediatorControl.getInstance().setController(this); + + specialPane.getStyleClass().add("special-pane-as-border"); // UI hacks + + uploadStopBtn.setDisable(true); + selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); }); + uploadStopBtn.setOnAction(e->{ uploadBtnAction(); }); + + selectNspBtn.getStyleClass().add("buttonSelect"); + + this.btnUpStopImage = new Region(); + btnUpStopImage.getStyleClass().add("regionUpload"); + //uploadStopBtn.getStyleClass().remove("button"); + uploadStopBtn.getStyleClass().add("buttonUp"); + uploadStopBtn.setGraphic(btnUpStopImage); + + ObservableList choiceProtocolList = FXCollections.observableArrayList("TinFoil", "GoldLeaf"); + choiceProtocol.setItems(choiceProtocolList); + choiceProtocol.getSelectionModel().select(0); // TODO: shared settings + choiceProtocol.setOnAction(e->tableFilesListController.setNewProtocol(choiceProtocol.getSelectionModel().getSelectedItem())); // Add listener to notify tableView controller + tableFilesListController.setNewProtocol(choiceProtocol.getSelectionModel().getSelectedItem()); // Notify tableView controller + + this.previouslyOpenedPath = null; + + Region btnSwitchImage = new Region(); + btnSwitchImage.getStyleClass().add("regionLamp"); + switchThemeBtn.setGraphic(btnSwitchImage); + this.switchThemeBtn.setOnAction(e->switchTheme()); + + previouslyOpenedPath = AppPreferences.getInstance().getRecent(); + } + /** + * Changes UI theme on the go + * */ + private void switchTheme(){ + if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app_dark.css")) { + switchThemeBtn.getScene().getStylesheets().remove("/res/app_dark.css"); + switchThemeBtn.getScene().getStylesheets().add("/res/app_light.css"); + } + else { + switchThemeBtn.getScene().getStylesheets().remove("/res/app_light.css"); + switchThemeBtn.getScene().getStylesheets().add("/res/app_dark.css"); + } + } + /** + * Functionality for selecting NSP button. + * Uses setReady and setNotReady to simplify code readability. + * */ + private void selectFilesBtnAction(){ + List filesList; + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("btnFileOpen")); + + 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("NSP ROM", "*.nsp")); + + filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow()); + if (filesList != null && !filesList.isEmpty()) { + tableFilesListController.setFiles(filesList); + uploadStopBtn.setDisable(false); + previouslyOpenedPath = filesList.get(0).getParent(); + } + else{ + tableFilesListController.setFiles(null); + uploadStopBtn.setDisable(true); + } + } + /** + * It's button listener when no transmission executes + * */ + private void uploadBtnAction(){ + if (usbThread == null || !usbThread.isAlive()){ + List nspToUpload; + if ((nspToUpload = tableFilesListController.getFiles()) == null) { + resourceBundle.getString("logsNoFolderFileSelected"); + return; + }else { + logArea.setText(resourceBundle.getString("logsFilesToUploadTitle")+"\n"); + for (File item: nspToUpload) + logArea.appendText(" "+item.getAbsolutePath()+"\n"); + } + UsbCommunications usbCommunications = new UsbCommunications(nspToUpload, choiceProtocol.getSelectionModel().getSelectedItem()); + usbThread = new Thread(usbCommunications); + usbThread.start(); + } + } + /** + * It's button listener when transmission in progress + * */ + private void stopBtnAction(){ + if (usbThread != null && usbThread.isAlive()){ + usbThread.interrupt(); + } + } + /** + * This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission" + * Called from mediator + * */ + public void notifyTransmissionStarted(boolean isTransmissionStarted){ + if (isTransmissionStarted) { + selectNspBtn.setDisable(true); + uploadStopBtn.setOnAction(e->{ stopBtnAction(); }); + + uploadStopBtn.setText(resourceBundle.getString("btnStop")); + + btnUpStopImage.getStyleClass().remove("regionUpload"); + btnUpStopImage.getStyleClass().add("regionStop"); + + uploadStopBtn.getStyleClass().remove("buttonUp"); + uploadStopBtn.getStyleClass().add("buttonStop"); + } + else { + selectNspBtn.setDisable(false); + uploadStopBtn.setOnAction(e->{ uploadBtnAction(); }); + + uploadStopBtn.setText(resourceBundle.getString("btnUpload")); + + btnUpStopImage.getStyleClass().remove("regionStop"); + btnUpStopImage.getStyleClass().add("regionUpload"); + + uploadStopBtn.getStyleClass().remove("buttonStop"); + uploadStopBtn.getStyleClass().add("buttonUp"); + } + } + /** + * Save preferences before exit + * */ + public void exit(){ + AppPreferences.getInstance().setTheme(switchThemeBtn.getScene().getStylesheets().get(0)); + AppPreferences.getInstance().setRecent(previouslyOpenedPath); + } +} diff --git a/src/main/java/nsusbloader/Controllers/NSLRowModel.java b/src/main/java/nsusbloader/Controllers/NSLRowModel.java new file mode 100644 index 0000000..1765eea --- /dev/null +++ b/src/main/java/nsusbloader/Controllers/NSLRowModel.java @@ -0,0 +1,54 @@ +package nsusbloader.Controllers; + +import nsusbloader.NSLDataTypes.EFileStatus; + +import java.io.File; + +public class NSLRowModel { + + private String status; + private File nspFile; + private String nspFileName; + private String nspFileSize; + private boolean markForUpload; + + NSLRowModel(File nspFile, boolean checkBoxValue){ + this.nspFile = nspFile; + this.markForUpload = checkBoxValue; + this.nspFileName = nspFile.getName(); + this.nspFileSize = String.format("%.2f", nspFile.length()/1024.0/1024.0); + this.status = ""; + } + // Model methods start + public String getStatus(){ + return status; + } + public String getNspFileName(){ + return nspFileName; + } + public String getNspFileSize() { return nspFileSize; } + public boolean isMarkForUpload() { + return markForUpload; + } + // Model methods end + + public void setMarkForUpload(boolean value){ + markForUpload = value; + } + public File getNspFile(){ return nspFile; } + public void setStatus(EFileStatus status){ // TODO: Localization + switch (status){ + case UPLOADED: + this.status = "Success"; + markForUpload = false; + break; + case FAILED: + this.status = "Failed"; + break; + case INCORRECT_FILE_FAILED: + this.status = "Failed: Incorrect file"; + markForUpload = false; + break; + } + } +} diff --git a/src/main/java/nsusbloader/Controllers/NSTableViewController.java b/src/main/java/nsusbloader/Controllers/NSTableViewController.java new file mode 100644 index 0000000..0252da9 --- /dev/null +++ b/src/main/java/nsusbloader/Controllers/NSTableViewController.java @@ -0,0 +1,177 @@ +package nsusbloader.Controllers; + +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.util.Callback; +import nsusbloader.NSLDataTypes.EFileStatus; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +public class NSTableViewController implements Initializable { + @FXML + private TableView table; + private ObservableList rowsObsLst; + + private String protocol; + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + rowsObsLst = FXCollections.observableArrayList(); + table.setPlaceholder(new Label()); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + TableColumn statusColumn = new TableColumn<>(resourceBundle.getString("tableStatusLbl")); + TableColumn fileNameColumn = new TableColumn<>(resourceBundle.getString("tableFileNameLbl")); + TableColumn fileSizeColumn = new TableColumn<>(resourceBundle.getString("tableSizeLbl")); + TableColumn uploadColumn = new TableColumn<>(resourceBundle.getString("tableUploadLbl")); + // See https://bugs.openjdk.java.net/browse/JDK-8157687 + statusColumn.setMinWidth(100.0); + statusColumn.setPrefWidth(100.0); + statusColumn.setMaxWidth(100.0); + statusColumn.setResizable(false); + + fileNameColumn.setMinWidth(25.0); + + fileSizeColumn.setMinWidth(120.0); + fileSizeColumn.setPrefWidth(120.0); + fileSizeColumn.setMaxWidth(120.0); + fileSizeColumn.setResizable(false); + + uploadColumn.setMinWidth(100.0); + uploadColumn.setPrefWidth(100.0); + uploadColumn.setMaxWidth(100.0); + uploadColumn.setResizable(false); + + statusColumn.setCellValueFactory(new PropertyValueFactory<>("status")); + fileNameColumn.setCellValueFactory(new PropertyValueFactory<>("nspFileName")); + fileSizeColumn.setCellValueFactory(new PropertyValueFactory<>("nspFileSize")); + // >< + uploadColumn.setCellValueFactory(new Callback, ObservableValue>() { + @Override + public ObservableValue call(TableColumn.CellDataFeatures paramFeatures) { + NSLRowModel model = paramFeatures.getValue(); + + SimpleBooleanProperty booleanProperty = new SimpleBooleanProperty(model.isMarkForUpload()); + + booleanProperty.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Boolean oldValue, Boolean newValue) { + model.setMarkForUpload(newValue); + restrictSelection(model); + } + }); + + return booleanProperty; + } + }); + + uploadColumn.setCellFactory(new Callback, TableCell>() { + @Override + public TableCell call(TableColumn paramFeatures) { + CheckBoxTableCell cell = new CheckBoxTableCell<>(); + return cell; + } + }); + + table.setItems(rowsObsLst); + table.getColumns().addAll(statusColumn, fileNameColumn, fileSizeColumn, uploadColumn); + } + /** + * See uploadColumn callback. In case of GoldLeaf we have to restrict selection + * */ + private void restrictSelection(NSLRowModel modelChecked){ + if (!protocol.equals("TinFoil") && rowsObsLst.size() > 1) { // Tinfoil doesn't need any restrictions. If only one file in list, also useless + for (NSLRowModel model: rowsObsLst){ + if (model != modelChecked) + model.setMarkForUpload(false); + } + table.refresh(); + } + } + /** + * Add files when user selected them + * */ + public void setFiles(List files){ + rowsObsLst.clear(); // TODO: consider table refresh + if (files == null) { + return; + } + if (protocol.equals("TinFoil")){ + for (File nspFile: files){ + rowsObsLst.add(new NSLRowModel(nspFile, true)); + } + } + else { + rowsObsLst.clear(); + for (File nspFile: files){ + rowsObsLst.add(new NSLRowModel(nspFile, false)); + } + rowsObsLst.get(0).setMarkForUpload(true); + } + } + /** + * Return files ready for upload. Requested from NSLMainController only + * @return null if no files marked for upload + * List if there are files + * */ + public List getFiles(){ + List files = new ArrayList<>(); + if (rowsObsLst.isEmpty()) + return null; + else { + for (NSLRowModel model: rowsObsLst){ + if (model.isMarkForUpload()) + files.add(model.getNspFile()); + } + if (!files.isEmpty()) + return files; + else + return null; + } + } + /** + * Update files in case something is wrong. Requested from UsbCommunications + * */ + public void setFileStatus(String fileName, EFileStatus status){ + for (NSLRowModel model: rowsObsLst){ + if (model.getNspFileName().equals(fileName)){ + model.setStatus(status); + } + } + table.refresh(); + } + /** + * Called if selected different USB protocol + * */ + public void setNewProtocol(String newProtocol){ + protocol = newProtocol; + if (rowsObsLst.isEmpty()) + return; + if (newProtocol.equals("TinFoil")){ + for (NSLRowModel model: rowsObsLst) + model.setMarkForUpload(true); + } + else { + for (NSLRowModel model: rowsObsLst) + model.setMarkForUpload(false); + rowsObsLst.get(0).setMarkForUpload(true); + } + table.refresh(); + } + +} diff --git a/src/main/java/nsusbloader/MediatorControl.java b/src/main/java/nsusbloader/MediatorControl.java index 66be7e2..5544864 100644 --- a/src/main/java/nsusbloader/MediatorControl.java +++ b/src/main/java/nsusbloader/MediatorControl.java @@ -1,25 +1,28 @@ package nsusbloader; -class MediatorControl { - private boolean isTransferActive = false; +import nsusbloader.Controllers.NSLMainController; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class MediatorControl { + private AtomicBoolean isTransferActive = new AtomicBoolean(false); // Overcoded just for sure private NSLMainController applicationController; - static MediatorControl getInstance(){ + public static MediatorControl getInstance(){ return MediatorControlHold.INSTANCE; } private static class MediatorControlHold { private static final MediatorControl INSTANCE = new MediatorControl(); } - void registerController(NSLMainController controller){ + public void setController(NSLMainController controller){ this.applicationController = controller; } + NSLMainController getContoller(){ return this.applicationController; } - synchronized void setTransferActive(boolean state) { - isTransferActive = state; + public synchronized void setTransferActive(boolean state) { + isTransferActive.set(state); applicationController.notifyTransmissionStarted(state); } - synchronized boolean getTransferActive() { - return this.isTransferActive; - } + public synchronized boolean getTransferActive() { return this.isTransferActive.get(); } } diff --git a/src/main/java/nsusbloader/MessagesConsumer.java b/src/main/java/nsusbloader/MessagesConsumer.java index bedf460..66c6af9 100644 --- a/src/main/java/nsusbloader/MessagesConsumer.java +++ b/src/main/java/nsusbloader/MessagesConsumer.java @@ -3,8 +3,11 @@ package nsusbloader; import javafx.animation.AnimationTimer; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; +import nsusbloader.Controllers.NSTableViewController; +import nsusbloader.NSLDataTypes.EFileStatus; import java.util.ArrayList; +import java.util.HashMap; import java.util.concurrent.BlockingQueue; public class MessagesConsumer extends AnimationTimer { @@ -13,18 +16,24 @@ public class MessagesConsumer extends AnimationTimer { private final BlockingQueue progressQueue; private final ProgressBar progressBar; + private final HashMap statusMap; + private final NSTableViewController tableViewController; private boolean isInterrupted; - MessagesConsumer(BlockingQueue msgQueue, TextArea logsArea, BlockingQueue progressQueue, ProgressBar progressBar){ - this.msgQueue = msgQueue; - this.logsArea = logsArea; + MessagesConsumer(BlockingQueue msgQueue, BlockingQueue progressQueue, HashMap statusMap){ + this.isInterrupted = false; + + this.msgQueue = msgQueue; + this.logsArea = MediatorControl.getInstance().getContoller().logArea; - this.progressBar = progressBar; this.progressQueue = progressQueue; + this.progressBar = MediatorControl.getInstance().getContoller().progressBar; + + this.statusMap = statusMap; + this.tableViewController = MediatorControl.getInstance().getContoller().tableFilesListController; progressBar.setProgress(0.0); - this.isInterrupted = false; MediatorControl.getInstance().setTransferActive(true); } @@ -40,15 +49,18 @@ public class MessagesConsumer extends AnimationTimer { if (progressRecieved > 0) progress.forEach(prg -> progressBar.setProgress(prg)); - if (isInterrupted) { + if (isInterrupted) { // It's safe 'cuz it's could't be interrupted while HashMap populating MediatorControl.getInstance().setTransferActive(false); progressBar.setProgress(0.0); + + if (statusMap.size() > 0) + for (String key : statusMap.keySet()) + tableViewController.setFileStatus(key, statusMap.get(key)); this.stop(); } - //TODO } void interrupt(){ this.isInterrupted = true; } -} +} \ No newline at end of file diff --git a/src/main/java/nsusbloader/NSLDataTypes/EFileStatus.java b/src/main/java/nsusbloader/NSLDataTypes/EFileStatus.java new file mode 100644 index 0000000..674521a --- /dev/null +++ b/src/main/java/nsusbloader/NSLDataTypes/EFileStatus.java @@ -0,0 +1,5 @@ +package nsusbloader.NSLDataTypes; + +public enum EFileStatus { + UPLOADED, INCORRECT_FILE_FAILED, FAILED +} diff --git a/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java b/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java new file mode 100644 index 0000000..5d88ea5 --- /dev/null +++ b/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java @@ -0,0 +1,5 @@ +package nsusbloader.NSLDataTypes; + +public enum EMsgType { + PASS, FAIL, INFO, WARNING +} diff --git a/src/main/java/nsusbloader/NSLMain.java b/src/main/java/nsusbloader/NSLMain.java index d34fd56..6e1d0d0 100644 --- a/src/main/java/nsusbloader/NSLMain.java +++ b/src/main/java/nsusbloader/NSLMain.java @@ -1,11 +1,3 @@ -/** - Name: NSL-USBFoil - @author Dmitry Isaenko - License: GNU GPL v.3 - @see https://github.com/developersu/ - @see https://developersu.blogspot.com/ - 2019, Russia - */ package nsusbloader; import javafx.application.Application; @@ -14,22 +6,24 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import nsusbloader.Controllers.NSLMainController; import java.util.Locale; import java.util.ResourceBundle; public class NSLMain extends Application { - static final String appVersion = "v0.1"; + public static final String appVersion = "v0.2"; @Override public void start(Stage primaryStage) throws Exception{ + + FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml")); + ResourceBundle rb; if (Locale.getDefault().getISO3Language().equals("rus")) rb = ResourceBundle.getBundle("locale", new Locale("ru")); else rb = ResourceBundle.getBundle("locale", new Locale("en")); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml")); - loader.setResources(rb); Parent root = loader.load(); @@ -44,7 +38,9 @@ public class NSLMain extends Application { primaryStage.setMinWidth(600); primaryStage.setMinHeight(375); Scene mainScene = new Scene(root, 800, 400); - mainScene.getStylesheets().add("/res/app.css"); + + mainScene.getStylesheets().add(AppPreferences.getInstance().getTheme()); + primaryStage.setScene(mainScene); primaryStage.show(); @@ -53,6 +49,9 @@ public class NSLMain extends Application { if(! ServiceWindow.getConfirmationWindow(rb.getString("windowTitleConfirmExit"), rb.getString("windowBodyConfirmExit"))) e.consume(); }); + + NSLMainController controller = loader.getController(); + primaryStage.setOnHidden(e-> controller.exit()); } public static void main(String[] args) { diff --git a/src/main/java/nsusbloader/NSLMainController.java b/src/main/java/nsusbloader/NSLMainController.java deleted file mode 100644 index ae5febd..0000000 --- a/src/main/java/nsusbloader/NSLMainController.java +++ /dev/null @@ -1,133 +0,0 @@ -package nsusbloader; - -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.ProgressBar; -import javafx.scene.control.TextArea; -import javafx.scene.layout.Region; -import javafx.stage.FileChooser; - -import java.io.File; -import java.net.URL; -import java.util.List; -import java.util.ResourceBundle; - -public class NSLMainController implements Initializable { - - private ResourceBundle resourceBundle; - - private List nspToUpload; - - @FXML - private TextArea logArea; - @FXML - private Button selectNspBtn; - @FXML - private Button uploadStopBtn; - private Region btnUpStopImage; - @FXML - private ProgressBar progressBar; - - private Thread usbThread; - - @Override - public void initialize(URL url, ResourceBundle rb) { - this.resourceBundle = rb; - logArea.setText(rb.getString("logsGreetingsMessage")+" "+NSLMain.appVersion+"!\n"); - if (System.getProperty("os.name").toLowerCase().startsWith("lin")) - if (!System.getProperty("user.name").equals("root")) - logArea.appendText(rb.getString("logsEnteredAsMsg1")+System.getProperty("user.name")+"\n"+rb.getString("logsEnteredAsMsg2") + "\n"); - - logArea.appendText(rb.getString("logsGreetingsMessage2")+"\n"); - - MediatorControl.getInstance().registerController(this); - - uploadStopBtn.setDisable(true); - selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); }); - uploadStopBtn.setOnAction(e->{ uploadBtnAction(); }); - - this.btnUpStopImage = new Region(); - btnUpStopImage.getStyleClass().add("regionUpload"); - //uploadStopBtn.getStyleClass().remove("button"); - uploadStopBtn.getStyleClass().add("buttonUp"); - uploadStopBtn.setGraphic(btnUpStopImage); - } - /** - * Functionality for selecting NSP button. - * Uses setReady and setNotReady to simplify code readability. - * */ - private void selectFilesBtnAction(){ - List filesList; - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(resourceBundle.getString("btnFileOpen")); - fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp")); - - filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow()); - if (filesList != null && !filesList.isEmpty()) - setReady(filesList); - else - setNotReady(resourceBundle.getString("logsNoFolderFileSelected")); - } - private void setReady(List filesList){ - logArea.setText(resourceBundle.getString("logsFilesToUploadTitle")+"\n"); - for (File item: filesList) - logArea.appendText(" "+item.getAbsolutePath()+"\n"); - nspToUpload = filesList; - uploadStopBtn.setDisable(false); - } - private void setNotReady(String whyNotReady){ - logArea.setText(whyNotReady); - nspToUpload = null; - uploadStopBtn.setDisable(true); - } - /** - * It's button listener when no transmission executes - * */ - private void uploadBtnAction(){ - if (usbThread == null || !usbThread.isAlive()){ - UsbCommunications usbCommunications = new UsbCommunications(logArea, progressBar, nspToUpload); //todo: progress bar - usbThread = new Thread(usbCommunications); - usbThread.start(); - } - } - /** - * It's button listener when transmission in progress - * */ - private void stopBtnAction(){ - if (usbThread != null && usbThread.isAlive()){ - usbThread.interrupt(); - } - } - /** - * This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission" - * Called from mediator - * */ - void notifyTransmissionStarted(boolean isTransmissionStarted){ - if (isTransmissionStarted) { - selectNspBtn.setDisable(true); - uploadStopBtn.setOnAction(e->{ stopBtnAction(); }); - - uploadStopBtn.setText(resourceBundle.getString("btnStop")); - - btnUpStopImage.getStyleClass().remove("regionUpload"); - btnUpStopImage.getStyleClass().add("regionStop"); - - uploadStopBtn.getStyleClass().remove("buttonUp"); - uploadStopBtn.getStyleClass().add("buttonStop"); - } - else { - selectNspBtn.setDisable(false); - uploadStopBtn.setOnAction(e->{ uploadBtnAction(); }); - - uploadStopBtn.setText(resourceBundle.getString("btnUpload")); - - btnUpStopImage.getStyleClass().remove("regionStop"); - btnUpStopImage.getStyleClass().add("regionUpload"); - - uploadStopBtn.getStyleClass().remove("buttonStop"); - uploadStopBtn.getStyleClass().add("buttonUp"); - } - } -} diff --git a/src/main/java/nsusbloader/PFS/NCAFile.java b/src/main/java/nsusbloader/PFS/NCAFile.java new file mode 100644 index 0000000..ea537ce --- /dev/null +++ b/src/main/java/nsusbloader/PFS/NCAFile.java @@ -0,0 +1,25 @@ +package nsusbloader.PFS; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Data class to hold NCA, tik, xml etc. meta-information + * */ +public class NCAFile { + //private int ncaNumber; + private byte[] ncaFileName; + private long ncaOffset; + private long ncaSize; + + //public void setNcaNumber(int ncaNumber){ this.ncaNumber = ncaNumber; } + public void setNcaFileName(byte[] ncaFileName) { this.ncaFileName = ncaFileName; } + public void setNcaOffset(long ncaOffset) { this.ncaOffset = ncaOffset; } + public void setNcaSize(long ncaSize) { this.ncaSize = ncaSize; } + + //public int getNcaNumber() {return this.ncaNumber; } + public byte[] getNcaFileName() { return ncaFileName; } + public byte[] getNcaFileNameLength() { return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFileName.length).array(); } + public long getNcaOffset() { return ncaOffset; } + public long getNcaSize() { return ncaSize; } +} diff --git a/src/main/java/nsusbloader/PFS/PFSProvider.java b/src/main/java/nsusbloader/PFS/PFSProvider.java new file mode 100644 index 0000000..2c22593 --- /dev/null +++ b/src/main/java/nsusbloader/PFS/PFSProvider.java @@ -0,0 +1,243 @@ +package nsusbloader.PFS; + +import nsusbloader.NSLDataTypes.EMsgType; +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; + +/** + * Used in GoldLeaf USB protocol + * */ +public class PFSProvider { + private static final byte[] PFS0 = new byte[]{(byte)0x50, (byte)0x46, (byte)0x53, (byte)0x30}; // PFS0, and what did you think? + + private BlockingQueue msgQueue; + private 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("PFS File not founnd: \n "+fnfe.getMessage(), EMsgType.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("PFS Start NSP file analyze for ["+nspFileName+"]", EMsgType.INFO); + try { + byte[] fileStartingBytes = new byte[12]; + // Read PFS0, files count, header, padding (4 zero bytes) + if (randAccessFile.read(fileStartingBytes) == 12) + printLog("PFS Read file starting bytes.", EMsgType.PASS); + else { + printLog("PFS Read file starting bytes.", EMsgType.FAIL); + randAccessFile.close(); + return false; + } + // Check PFS0 + if (Arrays.equals(PFS0, Arrays.copyOfRange(fileStartingBytes, 0, 4))) + printLog("PFS Read 'PFS0'.", EMsgType.PASS); + else { + printLog("PFS Read 'PFS0'.", EMsgType.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("PFS Read files count [" + filesCount + "]", EMsgType.PASS); + } + else { + printLog("PFS Read files count", EMsgType.FAIL); + randAccessFile.close(); + return false; + } + // Get header + header = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 8, 12)).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (header > 0 ) + printLog("PFS Read header ["+header+"]", EMsgType.PASS); + else { + printLog("PFS Read header ", EMsgType.FAIL); + randAccessFile.close(); + return false; + } + //********************************************************************************************* + // Create NCA set + this.ncaFiles = new NCAFile[filesCount]; + // Collect files from NSP + byte[] ncaInfoArr = new byte[24]; // should be unsigned long, but.. java.. u know my pain man + + HashMap ncaNameOffsets = new LinkedHashMap<>(); + + int offset; + long nca_offset; + long nca_size; + long nca_name_offset; + + for (int i=0; i= 0?EMsgType.PASS:EMsgType.WARNING); + printLog(" NCA size check: "+nca_size, nca_size >= 0?EMsgType.PASS: EMsgType.WARNING); + printLog(" NCA name offset check "+nca_name_offset, nca_name_offset >= 0?EMsgType.PASS:EMsgType.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("PFS Final padding check", EMsgType.PASS); + else + printLog("PFS Final padding check", EMsgType.WARNING); + + // Calculate position including header for body size offset + bodySize = randAccessFile.getFilePointer()+header; + //********************************************************************************************* + // Collect file names from NCAs + printLog("PFS Collecting file names", EMsgType.INFO); + List ncaFN; // Temporary + byte[] b = new byte[1]; // Temporary + for (int i=0; i(); + randAccessFile.seek(filesCount*24+16+ncaNameOffsets.get(i)); // Files cont * 24(bit for each meta-data) + 4 bytes goes after all of them + 12 bit what were in the beginning + while ((randAccessFile.read(b)) != -1){ + if (b[0] == 0x00) + break; + else + ncaFN.add(b[0]); + } + byte[] exchangeTempArray = new byte[ncaFN.size()]; + for (int j=0; j < ncaFN.size(); j++) + exchangeTempArray[j] = ncaFN.get(j); + // Find and store ticket (.tik) + if (new String(exchangeTempArray, StandardCharsets.UTF_8).toLowerCase().endsWith(".tik")) + this.ticketID = i; + this.ncaFiles[i].setNcaFileName(Arrays.copyOf(exchangeTempArray, exchangeTempArray.length)); + } + randAccessFile.close(); + } + catch (IOException ioe){ + printLog("PFS Failed NSP file analyze for ["+nspFileName+"]\n "+ioe.getMessage(), EMsgType.FAIL); + ioe.printStackTrace(); + } + printLog("PFS Finish NSP file analyze for ["+nspFileName+"]", EMsgType.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, EMsgType 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; + } + }catch (InterruptedException ie){ + ie.printStackTrace(); //TODO: ??? + } + } +} diff --git a/src/main/java/nsusbloader/RainbowHexDump.java b/src/main/java/nsusbloader/RainbowHexDump.java new file mode 100644 index 0000000..ee428b2 --- /dev/null +++ b/src/main/java/nsusbloader/RainbowHexDump.java @@ -0,0 +1,31 @@ +package nsusbloader; + +import java.nio.charset.StandardCharsets; + +/** + * Debug tool like hexdump <3 + */ +public class RainbowHexDump { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BLACK = "\u001B[30m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_BLUE = "\u001B[34m"; + private static final String ANSI_PURPLE = "\u001B[35m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_WHITE = "\u001B[37m"; + + + public static void hexDumpUTF8(byte[] byteArray){ + System.out.print(ANSI_BLUE); + for (int i=0; i < byteArray.length; i++) + System.out.print(String.format("%02d-", i%100)); + System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET); + for (byte b: byteArray) + System.out.print(String.format("%02x ", b)); + System.out.print("\t\t\t" + + new String(byteArray, StandardCharsets.UTF_8) + + "\n"); + } +} diff --git a/src/main/java/nsusbloader/ServiceWindow.java b/src/main/java/nsusbloader/ServiceWindow.java index c362148..2982201 100644 --- a/src/main/java/nsusbloader/ServiceWindow.java +++ b/src/main/java/nsusbloader/ServiceWindow.java @@ -11,7 +11,7 @@ public class ServiceWindow { * Create window with notification * */ /* // not used - static void getErrorNotification(String title, String body){ + public static void getErrorNotification(String title, String body){ Alert alertBox = new Alert(Alert.AlertType.ERROR); alertBox.setTitle(title); alertBox.setHeaderText(null); @@ -27,7 +27,7 @@ public class ServiceWindow { /** * Create notification window with confirm/deny * */ - static boolean getConfirmationWindow(String title, String body){ + public static boolean getConfirmationWindow(String title, String body){ Alert alertBox = new Alert(Alert.AlertType.CONFIRMATION); alertBox.setTitle(title); alertBox.setHeaderText(null); @@ -35,12 +35,9 @@ public class ServiceWindow { alertBox.getDialogPane().setMinWidth(Region.USE_PREF_SIZE); alertBox.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); alertBox.setResizable(true); // Java bug workaround for JDR11/OpenJFX. TODO: nothing. really. - alertBox.getDialogPane().getStylesheets().add("/res/app.css"); + alertBox.getDialogPane().getStylesheets().add(AppPreferences.getInstance().getTheme()); Optional result = alertBox.showAndWait(); - if (result.get() == ButtonType.OK) - return true; - else - return false; + return (result.isPresent() && result.get() == ButtonType.OK); } } diff --git a/src/main/java/nsusbloader/UsbCommunications.java b/src/main/java/nsusbloader/UsbCommunications.java index 1992287..d0df921 100644 --- a/src/main/java/nsusbloader/UsbCommunications.java +++ b/src/main/java/nsusbloader/UsbCommunications.java @@ -1,8 +1,9 @@ package nsusbloader; import javafx.concurrent.Task; -import javafx.scene.control.ProgressBar; -import javafx.scene.control.TextArea; +import nsusbloader.NSLDataTypes.EFileStatus; +import nsusbloader.NSLDataTypes.EMsgType; +import nsusbloader.PFS.PFSProvider; import org.usb4java.*; import java.io.*; @@ -17,18 +18,22 @@ import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -class UsbCommunications extends Task { +public class UsbCommunications extends Task { private final int DEFAULT_INTERFACE = 0; private BlockingQueue msgQueue; - private BlockingQueue progressQueue ; - private enum MsgType {PASS, FAIL, INFO, WARNING} + private BlockingQueue progressQueue; + private HashMap statusMap; // BlockingQueue for literally one object. TODO: read more books ; replace to hashMap + private EFileStatus status = EFileStatus.FAILED; + private MessagesConsumer msgConsumer; private HashMap nspMap; private Context contextNS; private DeviceHandle handlerNS; + + private String protocol; /* Ok, here is a story. We will pass to NS only file names, not full path. => see nspMap where 'key' is a file name. File name itself should not be greater then 512 bytes, but in real world it's limited by OS to something like 256 bytes. @@ -40,13 +45,15 @@ class UsbCommunications extends Task { Since this application let user an ability (theoretically) to choose same files in different folders, the latest selected file will be added to the list and handled correctly. I have no idea why he/she will make a decision to do that. Just in case, we're good in this point. */ - UsbCommunications(TextArea logArea, ProgressBar progressBar, List nspList){ + public UsbCommunications(List nspList, String protocol){ + this.protocol = protocol; this.nspMap = new HashMap<>(); for (File f: nspList) nspMap.put(f.getName(), f); this.msgQueue = new LinkedBlockingQueue<>(); this.progressQueue = new LinkedBlockingQueue<>(); - this.msgConsumer = new MessagesConsumer(this.msgQueue, logArea, this.progressQueue, progressBar); + this.statusMap = new HashMap<>(); + this.msgConsumer = new MessagesConsumer(this.msgQueue, this.progressQueue, this.statusMap); } @Override @@ -54,28 +61,28 @@ class UsbCommunications extends Task { this.msgConsumer.start(); int result = -9999; - printLog("\tStart chain", MsgType.INFO); + printLog("\tStart chain", EMsgType.INFO); // Creating Context required by libusb. Optional. TODO: Consider removing. contextNS = new Context(); result = LibUsb.init(contextNS); if (result != LibUsb.SUCCESS) { - printLog("libusb initialization\n Returned: "+result, MsgType.FAIL); + printLog("libusb initialization\n Returned: "+result, EMsgType.FAIL); close(); return null; } else - printLog("libusb initialization", MsgType.PASS); + printLog("libusb initialization", EMsgType.PASS); // Searching for NS in devices: obtain list of all devices DeviceList deviceList = new DeviceList(); result = LibUsb.getDeviceList(contextNS, deviceList); if (result < 0) { - printLog("Get device list\n Returned: "+result, MsgType.FAIL); + printLog("Get device list\n Returned: "+result, EMsgType.FAIL); close(); return null; } else { - printLog("Get device list", MsgType.PASS); + printLog("Get device list", EMsgType.PASS); } // Searching for NS in devices: looking for NS DeviceDescriptor descriptor; @@ -84,14 +91,14 @@ class UsbCommunications extends Task { descriptor = new DeviceDescriptor(); // mmm.. leave it as is. result = LibUsb.getDeviceDescriptor(device, descriptor); if (result != LibUsb.SUCCESS){ - printLog("Read file descriptors for USB devices\n Returned: "+result, MsgType.FAIL); + printLog("Read file descriptors for USB devices\n Returned: "+result, EMsgType.FAIL); LibUsb.freeDeviceList(deviceList, true); close(); return null; } if ((descriptor.idVendor() == 0x057E) && descriptor.idProduct() == 0x3000){ deviceNS = device; - printLog("Read file descriptors for USB devices", MsgType.PASS); + printLog("Read file descriptors for USB devices", EMsgType.PASS); break; } } @@ -134,10 +141,10 @@ class UsbCommunications extends Task { ////////////////////////////////////////// DEBUG INFORMATION END ///////////////////////////////////////////// if (deviceNS != null){ - printLog("NS in connected USB devices found", MsgType.PASS); + printLog("NS in connected USB devices found", EMsgType.PASS); } else { - printLog("NS in connected USB devices not found\n Returned: "+result, MsgType.FAIL); + printLog("NS in connected USB devices not found", EMsgType.FAIL); close(); return null; } @@ -147,25 +154,25 @@ class UsbCommunications extends Task { if (result != LibUsb.SUCCESS) { switch (result){ case LibUsb.ERROR_ACCESS: - printLog("Open NS USB device\n Returned: ERROR_ACCESS", MsgType.FAIL); - printLog("Double check that you have administrator privileges (you're 'root') or check 'udev' rules set for this user (linux only)!",MsgType.INFO); + printLog("Open NS USB device\n Returned: ERROR_ACCESS", EMsgType.FAIL); + printLog("Double check that you have administrator privileges (you're 'root') or check 'udev' rules set for this user (linux only)!", EMsgType.INFO); break; case LibUsb.ERROR_NO_MEM: - printLog("Open NS USB device\n Returned: ERROR_NO_MEM", MsgType.FAIL); + printLog("Open NS USB device\n Returned: ERROR_NO_MEM", EMsgType.FAIL); break; case LibUsb.ERROR_NO_DEVICE: - printLog("Open NS USB device\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Open NS USB device\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); break; default: - printLog("Open NS USB device\n Returned:" + result, MsgType.FAIL); + printLog("Open NS USB device\n Returned:" + result, EMsgType.FAIL); } close(); return null; } else - printLog("Open NS USB device", MsgType.PASS); + printLog("Open NS USB device", EMsgType.PASS); - printLog("Free device list", MsgType.INFO); + printLog("Free device list", EMsgType.INFO); LibUsb.freeDeviceList(deviceList, true); // DO some stuff to connected NS @@ -174,140 +181,566 @@ class UsbCommunications extends Task { if (canDetach){ int usedByKernel = LibUsb.kernelDriverActive(handlerNS, DEFAULT_INTERFACE); if (usedByKernel == LibUsb.SUCCESS){ - printLog("Can proceed with libusb driver", MsgType.PASS); // we're good + printLog("Can proceed with libusb driver", EMsgType.PASS); // we're good } else { switch (usedByKernel){ case 1: // used by kernel result = LibUsb.detachKernelDriver(handlerNS, DEFAULT_INTERFACE); - printLog("Detach kernel required", MsgType.INFO); + printLog("Detach kernel required", EMsgType.INFO); if (result != 0) { switch (result){ case LibUsb.ERROR_NOT_FOUND: - printLog("Detach kernel\n Returned: ERROR_NOT_FOUND", MsgType.FAIL); + printLog("Detach kernel\n Returned: ERROR_NOT_FOUND", EMsgType.FAIL); break; case LibUsb.ERROR_INVALID_PARAM: - printLog("Detach kernel\n Returned: ERROR_INVALID_PARAM", MsgType.FAIL); + printLog("Detach kernel\n Returned: ERROR_INVALID_PARAM", EMsgType.FAIL); break; case LibUsb.ERROR_NO_DEVICE: - printLog("Detach kernel\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Detach kernel\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); break; case LibUsb.ERROR_NOT_SUPPORTED: // Should never appear only if libusb buggy - printLog("Detach kernel\n Returned: ERROR_NOT_SUPPORTED", MsgType.FAIL); + printLog("Detach kernel\n Returned: ERROR_NOT_SUPPORTED", EMsgType.FAIL); break; default: - printLog("Detach kernel\n Returned: " + result, MsgType.FAIL); + printLog("Detach kernel\n Returned: " + result, EMsgType.FAIL); break; } close(); return null; } else { - printLog("Detach kernel", MsgType.PASS); + printLog("Detach kernel", EMsgType.PASS); break; } case LibUsb.ERROR_NO_DEVICE: - printLog("Can't proceed with libusb driver\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Can't proceed with libusb driver\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); break; case LibUsb.ERROR_NOT_SUPPORTED: - printLog("Can't proceed with libusb driver\n Returned: ERROR_NOT_SUPPORTED", MsgType.FAIL); + printLog("Can't proceed with libusb driver\n Returned: ERROR_NOT_SUPPORTED", EMsgType.FAIL); break; default: - printLog("Can't proceed with libusb driver\n Returned: "+result, MsgType.FAIL); + printLog("Can't proceed with libusb driver\n Returned: "+result, EMsgType.FAIL); } } } else - printLog("libusb doesn't supports function 'CAP_SUPPORTS_DETACH_KERNEL_DRIVER'. Proceeding.", MsgType.WARNING); + printLog("libusb doesn't supports function 'CAP_SUPPORTS_DETACH_KERNEL_DRIVER'. Proceeding.", EMsgType.WARNING); // Set configuration (soft reset if needed) result = LibUsb.setConfiguration(handlerNS, 1); // 1 - configuration all we need if (result != LibUsb.SUCCESS){ switch (result){ case LibUsb.ERROR_NOT_FOUND: - printLog("Set active configuration to device\n Returned: ERROR_NOT_FOUND", MsgType.FAIL); + printLog("Set active configuration to device\n Returned: ERROR_NOT_FOUND", EMsgType.FAIL); break; case LibUsb.ERROR_BUSY: - printLog("Set active configuration to device\n Returned: ERROR_BUSY", MsgType.FAIL); + printLog("Set active configuration to device\n Returned: ERROR_BUSY", EMsgType.FAIL); break; case LibUsb.ERROR_NO_DEVICE: - printLog("Set active configuration to device\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Set active configuration to device\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); break; case LibUsb.ERROR_INVALID_PARAM: - printLog("Set active configuration to device\n Returned: ERROR_INVALID_PARAM", MsgType.FAIL); + printLog("Set active configuration to device\n Returned: ERROR_INVALID_PARAM", EMsgType.FAIL); break; default: - printLog("Set active configuration to device\n Returned: "+result, MsgType.FAIL); + printLog("Set active configuration to device\n Returned: "+result, EMsgType.FAIL); break; } close(); return null; } else { - printLog("Set active configuration to device.", MsgType.PASS); + printLog("Set active configuration to device.", EMsgType.PASS); } // Claim interface result = LibUsb.claimInterface(handlerNS, DEFAULT_INTERFACE); if (result != LibUsb.SUCCESS) { - printLog("Claim interface\n Returned: "+result, MsgType.FAIL); + printLog("Claim interface\n Returned: "+result, EMsgType.FAIL); close(); return null; } else - printLog("Claim interface", MsgType.PASS); + printLog("Claim interface", EMsgType.PASS); - - - // Send list of NSP files: - // Proceed "TUL0" - if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30} - printLog("Send list of files: handshake", MsgType.FAIL); - close(); - return null; + //-------------------------------------------------------------------------------------------------------------- + if (protocol.equals("TinFoil")) { + new TinFoil(); + } else { + new GoldLeaf(); } - else - printLog("Send list of files: handshake", MsgType.PASS); - //Collect file names - StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder - for(String nspFileName: nspMap.keySet()) - nspListNamesBuilder.append(nspFileName+'\n'); // And here we come with java string default encoding (UTF-16) - - byte[] nspListNames = nspListNamesBuilder.toString().getBytes(StandardCharsets.UTF_8); - ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format - byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me. - byte[] nspListSize = byteBuffer.array(); // TODO: rewind? not sure.. - //byteBuffer.reset(); - - // Sending NSP list - if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes... - printLog("Send list of files: send length.", MsgType.FAIL); - close(); - return null; - } else - printLog("Send list of files: send length.", MsgType.PASS); - if (!writeToUsb(new byte[8])) { // 8 zero bytes goes... - printLog("Send list of files: send padding.", MsgType.FAIL); - close(); - return null; - } - else - printLog("Send list of files: send padding.", MsgType.PASS); - if (!writeToUsb(nspListNames)) { // list of the names goes... - printLog("Send list of files: send list itself.", MsgType.FAIL); - close(); - return null; - } - else - printLog("Send list of files: send list itself.", MsgType.PASS); - - proceedCommands(); close(); - printLog("\tEnd chain", MsgType.INFO); + printLog("\tEnd chain", EMsgType.INFO); return null; } + /** + * Tinfoil processing + * */ + private class TinFoil{ + TinFoil(){ + + if (!sendListOfNSP()) + return; + + if (proceedCommands()) // REPORT SUCCESS + status = EFileStatus.UPLOADED; // Don't change status that is already set to FAILED + } + /** + * Send what NSP will be transferred + * */ + 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("TF Send list of files: handshake", EMsgType.FAIL); + return false; + } + else + printLog("TF Send list of files: handshake", EMsgType.PASS); + //Collect file names + StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder + for(String nspFileName: nspMap.keySet()) { + nspListNamesBuilder.append(nspFileName); // And here we come with java string default encoding (UTF-16) + nspListNamesBuilder.append('\n'); + } + + 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 + printLog("TF Send list of files", EMsgType.INFO); + if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes... + printLog(" [send list length]", EMsgType.FAIL); + return false; + } + printLog(" [send list length]", EMsgType.PASS); + + if (!writeToUsb(new byte[8])) { // 8 zero bytes goes... + printLog(" [send padding]", EMsgType.FAIL); + return false; + } + printLog(" [send padding]", EMsgType.PASS); + + if (!writeToUsb(nspListNames)) { // list of the names goes... + printLog(" [send list itself]", EMsgType.FAIL); + return false; + } + printLog(" [send list itself]", EMsgType.PASS); + + return true; + } + /** + * After we sent commands to NS, this chain starts + * */ + private boolean proceedCommands(){ + printLog("TF Awaiting for NS commands.", EMsgType.INFO); + + /* byte[] magic = new byte[4]; + ByteBuffer bb = StandardCharsets.UTF_8.encode("TUC0").rewind().get(magic); + // Let's rephrase this 'string' */ + final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean) + + byte[] receivedArray; + + while (true){ + if (Thread.currentThread().isInterrupted()) // Check if user interrupted process. + return false; + receivedArray = readFromUsb(); + if (receivedArray == null) + return false; // catches exception + + if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this + continue; + + // 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus: + // BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0. + if (receivedArray[8] == 0x00){ //0x00 - exit + printLog("TF Received EXIT command. Terminating.", EMsgType.PASS); + return true; // All interaction with USB device should be ended (expected); + } + else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack). + printLog("TF Received FILE_RANGE command. Proceeding: [0x0"+receivedArray[8]+"]", EMsgType.PASS); + /*// We can get in this pocket a length of file name (+32). Why +32? I dunno man.. Do we need this? Definitely not. This app can live without it. + long receivedSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 12,20)).order(ByteOrder.LITTLE_ENDIAN).getLong(); + logsArea.appendText("[V] Received FILE_RANGE command. Size: "+Long.toUnsignedString(receivedSize)+"\n"); // this shit returns string that will be chosen next '+32'. And, BTW, can't be greater then 512 + */ + if (!fileRangeCmd()) { + return false; // catches exception + } + } + } + } + /** + * This is what returns requested file (files) + * Executes multiple times + * @return 'true' if everything is ok + * 'false' is error/exception occurs + * */ + private boolean fileRangeCmd(){ + boolean isProgessBarInitiated = false; + + byte[] receivedArray; + // Here we take information of what other side wants + receivedArray = readFromUsb(); + if (receivedArray == null) + return false; + + // range_offset of the requested file. In the begining it will be 0x10. + long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb + byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8); // used (only) when we use sendResponse(). It's just simply. + long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb + /* Below, it's REAL NSP file name length that we sent before among others (WITHOUT +32 byes). It can't be greater then... see what is written in the beginning of this code. + We don't need this since in next pocket we'll get name itself UTF-8 encoded. Could be used to double-checks or something like that. + long receivedNspNameLen = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 16,24)).order(ByteOrder.LITTLE_ENDIAN).getLong(); */ + + // Requesting UTF-8 file name required: + receivedArray = readFromUsb(); + if (receivedArray == null) + return false; + + String receivedRequestedNSP = new String(receivedArray, StandardCharsets.UTF_8); + printLog("TF Reply to requested file: "+receivedRequestedNSP + +"\n Range Size: "+receivedRangeSize + +"\n Range Offset: "+receivedRangeOffset, EMsgType.INFO); + + // Sending response header + if (!sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply. + return false; + + try { + + BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspMap.get(receivedRequestedNSP))); // TODO: refactor? + byte[] bufferCurrent ;//= new byte[1048576]; // eq. Allocate 1mb + int bufferLength; + if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){ + printLog("TF Requested skip is out of file size. Nothing to transmit.", EMsgType.FAIL); + return false; + } + + long currentOffset = 0; + // 'End Offset' equal to receivedRangeSize. + int readPice = 8388608; // = 8Mb + + while (currentOffset < receivedRangeSize){ + if (Thread.currentThread().isInterrupted()) // Check if user interrupted process. + return true; + if ((currentOffset + readPice) >= receivedRangeSize ) + readPice = Math.toIntExact(receivedRangeSize - currentOffset); + //System.out.println("CO: "+currentOffset+"\t\tEO: "+receivedRangeSize+"\t\tRP: "+readPice); // TODO: NOTE: DEBUG + // updating progress bar (if a lot of data requested) START BLOCK + if (isProgessBarInitiated){ + try { + if (currentOffset+readPice == receivedRangeOffset){ + progressQueue.put(1.0); + isProgessBarInitiated = false; + } + else + progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0); + }catch (InterruptedException ie){ + getException().printStackTrace(); // TODO: Do something with this + } + } + else { + if ((readPice == 8388608) && (currentOffset == 0)) + isProgessBarInitiated = true; + } + // updating progress bar if needed END BLOCK + + bufferCurrent = new byte[readPice]; // TODO: not perfect moment, consider refactoring. + + bufferLength = bufferedInStream.read(bufferCurrent); + + if (bufferLength != -1){ + //write to USB + if (!writeToUsb(bufferCurrent)) { + printLog("TF Failure during NSP transmission.", EMsgType.FAIL); + return false; + } + currentOffset += readPice; + } + else { + printLog("TF Reading of stream suddenly ended.", EMsgType.WARNING); + return false; + } + + } + bufferedInStream.close(); + } catch (FileNotFoundException fnfe){ + printLog("TF FileNotFoundException:\n "+fnfe.getMessage(), EMsgType.FAIL); + fnfe.printStackTrace(); + return false; + } catch (IOException ioe){ + printLog("TF IOException:\n "+ioe.getMessage(), EMsgType.FAIL); + ioe.printStackTrace(); + return false; + } catch (ArithmeticException ae){ + printLog("TF ArithmeticException (can't cast end offset minus current to 'integer'):\n "+ae.getMessage(), EMsgType.FAIL); + ae.printStackTrace(); + return false; + } + + return true; + } + /** + * Send response header. + * @return true if everything OK + * false if failed + * */ + private boolean sendResponse(byte[] rangeSize){ // This method as separate function itself for application needed as a cookie in the middle of desert. + printLog("TF Sending response", EMsgType.INFO); + if (!writeToUsb(new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0' + (byte) 0x01, // CMD_TYPE_RESPONSE = 1 + (byte) 0x00, (byte) 0x00, (byte) 0x00, // kinda padding. Guys, didn't you want to use integer value for CMD semantic? + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00} ) // Send integer value of '1' in Little-endian format. + ){ + printLog(" [1/3]", EMsgType.FAIL); + return false; + } + printLog(" [1/3]", EMsgType.PASS); + if(!writeToUsb(rangeSize)) { // Send EXACTLY what has been received + printLog(" [2/3]", EMsgType.FAIL); + return false; + } + printLog(" [2/3]", EMsgType.PASS); + if(!writeToUsb(new byte[12])) { // kinda another one padding + printLog(" [3/3]", EMsgType.FAIL); + return false; + } + printLog(" [3/3]", EMsgType.PASS); + return true; + } + + } + /** + * GoldLeaf processing + * */ + 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(){ + printLog("===========================================================================", EMsgType.INFO); + PFSProvider pfsElement = new PFSProvider(nspMap.get(nspMap.keySet().toArray()[0]), msgQueue); + if (!pfsElement.init()) { + printLog("GL File provided have incorrect structure and won't be uploaded", EMsgType.FAIL); + status = EFileStatus.INCORRECT_FILE_FAILED; + return; + } + printLog("GL File structure validated and it will be uploaded", EMsgType.PASS); + + if (initGoldLeafProtocol(pfsElement)) + status = EFileStatus.UPLOADED; + // else - no change status that is already set to FAILED + } + private boolean initGoldLeafProtocol(PFSProvider pfsElement){ + // Go parse commands + byte[] readByte; + + // Go connect to GoldLeaf + if (writeToUsb(CMD_ConnectionRequest)) + printLog("GL Initiating GoldLeaf connection", EMsgType.PASS); + else { + printLog("GL Initiating GoldLeaf connection", EMsgType.FAIL); + return false; + } + while (true) { + readByte = readFromUsb(); + if (readByte == null) + return false; + if (Arrays.equals(readByte, CMD_ConnectionResponse)) { + if (!handleConnectionResponse(pfsElement)) + return false; + else + continue; + } + if (Arrays.equals(readByte, CMD_Start)) { + if (!handleStart(pfsElement)) + return false; + else + continue; + } + if (Arrays.equals(readByte, CMD_NSPContent)) { + if (!handleNSPContent(pfsElement, true)) + return false; + else + continue; + } + if (Arrays.equals(readByte, CMD_NSPTicket)) { + if (!handleNSPContent(pfsElement, false)) + return false; + else + continue; + } + if (Arrays.equals(readByte, CMD_Finish)) { + printLog("GL Closing GoldLeaf connection: Transfer successful.", EMsgType.PASS); + break; + } + } + return true; + } + /** + * ConnectionResponse command handler + * */ + private boolean handleConnectionResponse(PFSProvider pfsElement){ + printLog("GL 'ConnectionResonse' command:", EMsgType.INFO); + if (!writeToUsb(CMD_NSPName)) { + printLog(" [1/3]", EMsgType.FAIL); + return false; + } + printLog(" [1/3]", EMsgType.PASS); + + if (!writeToUsb(pfsElement.getBytesNspFileNameLength())) { + printLog(" [2/3]", EMsgType.FAIL); + return false; + } + printLog(" [2/3]", EMsgType.PASS); + + if (!writeToUsb(pfsElement.getBytesNspFileName())) { + printLog(" [3/3]", EMsgType.FAIL); + return false; + } + printLog(" [3/3]", EMsgType.PASS); + + return true; + } + /** + * Start command handler + * */ + private boolean handleStart(PFSProvider pfsElement){ + printLog("GL Handle 'Start' command:", EMsgType.INFO); + if (!writeToUsb(CMD_NSPData)) { + printLog(" [Send command]", EMsgType.FAIL); + return false; + } + printLog(" [Send command]", EMsgType.PASS); + + if (!writeToUsb(pfsElement.getBytesCountOfNca())) { + printLog(" [Send length]", EMsgType.FAIL); + return false; + } + printLog(" [Send length]", EMsgType.PASS); + + int ncaCount = pfsElement.getIntCountOfNca(); + printLog(" [Send information for "+ncaCount+" files]", EMsgType.INFO); + for (int i = 0; i < ncaCount; i++){ + if (!writeToUsb(pfsElement.getNca(i).getNcaFileNameLength())) { + printLog(" [1/4] File #"+i, EMsgType.FAIL); + return false; + } + printLog(" [1/4] File #"+i, EMsgType.PASS); + + if (!writeToUsb(pfsElement.getNca(i).getNcaFileName())) { + printLog(" [2/4] File #"+i, EMsgType.FAIL); + return false; + } + printLog(" [2/4] File #"+i, EMsgType.PASS); + if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getBodySize()+pfsElement.getNca(i).getNcaOffset()).array())) { // offset. real. + printLog(" [2/4] File #"+i, EMsgType.FAIL); + return false; + } + printLog(" [3/4] File #"+i, EMsgType.PASS); + if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getNca(i).getNcaSize()).array())) { // size + printLog(" [4/4] File #"+i, EMsgType.FAIL); + return false; + } + printLog(" [4/4] File #"+i, EMsgType.PASS); + } + 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) { + printLog("GL Handle 'Content' command", EMsgType.INFO); + byte[] readByte = readFromUsb(); + if (readByte == null || readByte.length != 4) { + printLog(" [Read requested ID]", EMsgType.FAIL); + return false; + } + requestedNcaID = ByteBuffer.wrap(readByte).order(ByteOrder.LITTLE_ENDIAN).getInt(); + printLog(" [Read requested ID = "+requestedNcaID+" ]", EMsgType.PASS); + } + else { + requestedNcaID = pfsElement.getNcaTicketID(); + printLog("GL Handle 'Ticket' command (ID = "+requestedNcaID+" )", EMsgType.INFO); + } + + 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){ + printLog(" Failed to read NCA ID "+requestedNcaID+". IO Exception:\n "+ioe.getMessage(), EMsgType.FAIL); + ioe.printStackTrace(); + return false; + } + return true; + } + } + //------------------------------------------------------------------------------------------------------------------ /** * Correct exit * */ @@ -318,201 +751,25 @@ class UsbCommunications extends Task { int result = LibUsb.releaseInterface(handlerNS, DEFAULT_INTERFACE); if (result != LibUsb.SUCCESS) - printLog("Release interface\n Returned: "+result+" (sometimes it's not an issue)", MsgType.WARNING); + printLog("Release interface\n Returned: "+result+" (sometimes it's not an issue)", EMsgType.WARNING); else - printLog("Release interface", MsgType.PASS); + printLog("Release interface", EMsgType.PASS); LibUsb.close(handlerNS); - printLog("Requested handler close", MsgType.INFO); + printLog("Requested handler close", EMsgType.INFO); } // close context in the end if (contextNS != null) { LibUsb.exit(contextNS); - printLog("Requested context close", MsgType.INFO); + printLog("Requested context close", EMsgType.INFO); } + + // Report status + for (String fileName: nspMap.keySet()) + statusMap.put(fileName, status); + msgConsumer.interrupt(); } - /** - * After we sent commands to NS, this chain starts - * */ - private void proceedCommands(){ - printLog("Awaiting for NS commands.", MsgType.INFO); - - /* byte[] magic = new byte[4]; - ByteBuffer bb = StandardCharsets.UTF_8.encode("TUC0").rewind().get(magic); - // Let's rephrase this 'string' */ - final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean) - - byte[] receivedArray; - - while (true){ - if (Thread.currentThread().isInterrupted()) // Check if user interrupted process. - return; - receivedArray = readFromUsb(); - if (receivedArray == null) - return; // catches exception - - if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this - continue; - - // 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus: - // BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0. - if (receivedArray[8] == 0x00){ //0x00 - exit - printLog("Received EXIT command. Terminating.", MsgType.PASS); - return; // All interaction with USB device should be ended (expected); - } - else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack). - printLog("Received FILE_RANGE command. Proceeding: [0x0"+receivedArray[8]+"]", MsgType.PASS); - /*// We can get in this pocket a length of file name (+32). Why +32? I dunno man.. Do we need this? Definitely not. This app can live without it. - long receivedSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 12,20)).order(ByteOrder.LITTLE_ENDIAN).getLong(); - logsArea.appendText("[V] Received FILE_RANGE command. Size: "+Long.toUnsignedString(receivedSize)+"\n"); // this shit returns string that will be chosen next '+32'. And, BTW, can't be greater then 512 - */ - if (!fileRangeCmd()) { - return; // catches exception - } - } - } - } - /** - * This is what returns requested file (files) - * Executes multiple times - * @return 'true' if everything is ok - * 'false' is error/exception occurs - * */ - private boolean fileRangeCmd(){ - boolean isProgessBarInitiated = false; - - byte[] receivedArray; - // Here we take information of what other side wants - receivedArray = readFromUsb(); - if (receivedArray == null) - return false; - - // range_offset of the requested file. In the begining it will be 0x10. - long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb - byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8); // used (only) when we use sendResponse(). It's just simply. - long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb - /* Below, it's REAL NSP file name length that we sent before among others (WITHOUT +32 byes). It can't be greater then... see what is written in the beginning of this code. - We don't need this since in next pocket we'll get name itself UTF-8 encoded. Could be used to double-checks or something like that. - long receivedNspNameLen = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 16,24)).order(ByteOrder.LITTLE_ENDIAN).getLong(); */ - - // Requesting UTF-8 file name required: - receivedArray = readFromUsb(); - if (receivedArray == null) - return false; - - String receivedRequestedNSP = new String(receivedArray, StandardCharsets.UTF_8); - printLog("Reply to requested file: "+receivedRequestedNSP - +"\n Range Size: "+receivedRangeSize - +"\n Range Offset: "+receivedRangeOffset, MsgType.INFO); - - // Sending response header - if (!sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply. - return false; - - // Read file starting: - // from Range Offset (receivedRangeOffset) - // to Range Size (receivedRangeSize) like end: receivedRangeOffset+receivedRangeSize - - try { - - BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspMap.get(receivedRequestedNSP))); // TODO: refactor? - byte[] bufferCurrent ;//= new byte[1048576]; // eq. Allocate 1mb - int bufferLength; - if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){ - printLog("Requested skip is out of File size. Nothing to transmit.", MsgType.FAIL); - return false; - } - - long currentOffset = 0; - // 'End Offset' equal to receivedRangeSize. - int readPice = 8388608; // = 8Mb - - while (currentOffset < receivedRangeSize){ - if (Thread.currentThread().isInterrupted()) // Check if user interrupted process. - return true; - if ((currentOffset + readPice) >= receivedRangeSize ) - readPice = Math.toIntExact(receivedRangeSize - currentOffset); - //System.out.println("CO: "+currentOffset+"\t\tEO: "+receivedRangeSize+"\t\tRP: "+readPice); // TODO: NOTE: -----------------------DEBUG----------------- - // updating progress bar (if a lot of data requested) START BLOCK - if (isProgessBarInitiated){ - try { - if (currentOffset+readPice == receivedRangeOffset){ - progressQueue.put(1.0); - isProgessBarInitiated = false; - } - else - progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0); - }catch (InterruptedException ie){ - getException().printStackTrace(); - } - } - else { - if ((readPice == 8388608) && (currentOffset == 0)) - isProgessBarInitiated = true; - } - // updating progress bar if needed END BLOCK - - bufferCurrent = new byte[readPice]; // TODO: not perfect moment, consider refactoring. - - bufferLength = bufferedInStream.read(bufferCurrent); - - if (bufferLength != -1){ - //write to USB - if (!writeToUsb(bufferCurrent)) { - printLog("Failure during NSP transmission.", MsgType.FAIL); - return false; - } - currentOffset += readPice; - } - else { - printLog("Unexpected reading of stream ended.", MsgType.WARNING); - return false; - } - - } - } catch (FileNotFoundException fnfe){ - printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL); - return false; - } catch (IOException ioe){ - printLog("IOException:\n"+ioe.getMessage(), MsgType.FAIL); - return false; - } catch (ArithmeticException ae){ - printLog("ArithmeticException (can't cast end offset minus current to 'integer'):\n"+ae.getMessage(), MsgType.FAIL); - return false; - } - - return true; - } - /** - * Send response header. - * @return true if everything OK - * false if failed - * */ - private boolean sendResponse(byte[] rangeSize){ // This method as separate function itself for application needed as a cookie in the middle of desert. - printLog("Sending response", MsgType.INFO); - if (!writeToUsb(new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0' - (byte) 0x01, // CMD_TYPE_RESPONSE = 1 - (byte) 0x00, (byte) 0x00, (byte) 0x00, // kinda padding. Guys, didn't you want to use integer value for CMD semantic? - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00} ) // Send integer value of '1' in Little-endian format. - ){ - printLog("[1/3]", MsgType.FAIL); - return false; - } - printLog("[1/3]", MsgType.PASS); - if(!writeToUsb(rangeSize)) { // Send EXACTLY what has been received - printLog("[2/3]", MsgType.FAIL); - return false; - } - printLog("[2/3]", MsgType.PASS); - if(!writeToUsb(new byte[12])) { // kinda another one padding - printLog("[3/3]", MsgType.FAIL); - return false; - } - printLog("[3/3]", MsgType.PASS); - return true; - } - /** * Sending any byte array to USB device * @return 'true' if no issues @@ -528,25 +785,25 @@ class UsbCommunications extends Task { if (result != LibUsb.SUCCESS){ switch (result){ case LibUsb.ERROR_TIMEOUT: - printLog("Data transfer (write) issue\n Returned: ERROR_TIMEOUT", MsgType.FAIL); + printLog("Data transfer (write) issue\n Returned: ERROR_TIMEOUT", EMsgType.FAIL); break; case LibUsb.ERROR_PIPE: //WUT?? I dunno man looks overkill in here.. - printLog("Data transfer (write) issue\n Returned: ERROR_PIPE", MsgType.FAIL); + printLog("Data transfer (write) issue\n Returned: ERROR_PIPE", EMsgType.FAIL); break; case LibUsb.ERROR_OVERFLOW: - printLog("Data transfer (write) issue\n Returned: ERROR_OVERFLOW", MsgType.FAIL); + printLog("Data transfer (write) issue\n Returned: ERROR_OVERFLOW", EMsgType.FAIL); break; case LibUsb.ERROR_NO_DEVICE: - printLog("Data transfer (write) issue\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Data transfer (write) issue\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); break; default: - printLog("Data transfer (write) issue\n Returned: "+result, MsgType.FAIL); + printLog("Data transfer (write) issue\n Returned: "+result, EMsgType.FAIL); } - printLog("Execution stopped", MsgType.FAIL); + printLog("Execution stopped", EMsgType.FAIL); return false; }else { if (writeBufTransferred.get() != message.length){ - printLog("Data transfer (write) issue\n Requested: "+message.length+"\n Transferred: "+writeBufTransferred.get(), MsgType.FAIL); + printLog("Data transfer (write) issue\n Requested: "+message.length+"\n Transferred: "+writeBufTransferred.get(), EMsgType.FAIL); return false; } else { @@ -557,7 +814,7 @@ class UsbCommunications extends Task { /** * Reading what USB device responded. * @return byte array if data read successful - * 'null' if read failed + * 'null' if read failed * */ private byte[] readFromUsb(){ ByteBuffer readBuffer = ByteBuffer.allocateDirect(512);// //readBuffer.order() equals BIG_ENDIAN; DON'T TOUCH. And we will always allocate readBuffer for max-size endpoint supports (512 bytes) @@ -570,21 +827,24 @@ class UsbCommunications extends Task { if (result != LibUsb.SUCCESS){ switch (result){ case LibUsb.ERROR_TIMEOUT: - printLog("Data transfer (read) issue\n Returned: ERROR_TIMEOUT", MsgType.FAIL); + printLog("Data transfer (read) issue\n Returned: ERROR_TIMEOUT", EMsgType.FAIL); break; case LibUsb.ERROR_PIPE: //WUT?? I dunno man looks overkill in here.. - printLog("Data transfer (read) issue\n Returned: ERROR_PIPE", MsgType.FAIL); + printLog("Data transfer (read) issue\n Returned: ERROR_PIPE", EMsgType.FAIL); break; case LibUsb.ERROR_OVERFLOW: - printLog("Data transfer (read) issue\n Returned: ERROR_OVERFLOW", MsgType.FAIL); + printLog("Data transfer (read) issue\n Returned: ERROR_OVERFLOW", EMsgType.FAIL); break; case LibUsb.ERROR_NO_DEVICE: - printLog("Data transfer (read) issue\n Returned: ERROR_NO_DEVICE", MsgType.FAIL); + printLog("Data transfer (read) issue\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL); + break; + case LibUsb.ERROR_IO: + printLog("Data transfer (read) issue\n Returned: ERROR_IO", EMsgType.FAIL); break; default: - printLog("Data transfer (read) issue\n Returned: "+result, MsgType.FAIL); + printLog("Data transfer (read) issue\n Returned: "+result, EMsgType.FAIL); } - printLog("Execution stopped", MsgType.FAIL); + printLog("Execution stopped", EMsgType.FAIL); return null; } else { int trans = readBufTransferred.get(); @@ -596,10 +856,11 @@ class UsbCommunications extends Task { return receivedBytes; } } + /** * This is what will print to textArea of the application. * */ - private void printLog(String message, MsgType type){ + private void printLog(String message, EMsgType type){ try { switch (type){ case PASS: @@ -620,21 +881,5 @@ class UsbCommunications extends Task { }catch (InterruptedException ie){ ie.printStackTrace(); } - } - /** - * 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"); - } - */ -} +} \ No newline at end of file diff --git a/src/main/resources/NSLMain.fxml b/src/main/resources/NSLMain.fxml index e3114b7..5ef03a6 100644 --- a/src/main/resources/NSLMain.fxml +++ b/src/main/resources/NSLMain.fxml @@ -2,23 +2,80 @@ + + + + + + + - + - + + + + + + + + + + +