From f8b60af41ba990bd3b844eee9aed188405405e01 Mon Sep 17 00:00:00 2001
From: Dmitry Isaenko <developer.su@gmail.com>
Date: Wed, 1 Feb 2023 12:43:47 +0300
Subject: [PATCH] FS drafts

---
 .../Controllers/PatchesController.java        |  43 +++-
 .../Utilities/patches/AHeuristic.java         |  45 ++++
 .../patches/{es => }/BinToAsmPrinter.java     |  47 +++-
 .../patches/{es => }/SimplyFind.java          |   2 +-
 .../Utilities/patches/es/EsNcaSearchTask.java |   2 +-
 .../Utilities/patches/es/EsPatch.java         |  10 +-
 .../Utilities/patches/es/EsPatchMaker.java    |   6 +-
 .../patches/es/finders/HeuristicEs1.java      |   7 +-
 .../patches/es/finders/HeuristicEs2.java      |   7 +-
 .../patches/es/finders/HeuristicEs3.java      |   7 +-
 .../patches/es/finders/HeuristicEsWizard.java |  18 +-
 .../patches/es/finders/IHeuristicEs.java      |  45 ----
 .../Utilities/patches/fs/FsNcaSearchTask.java |  50 +++++
 .../Utilities/patches/fs/FsPatch.java         | 208 ++++++++++++++++++
 .../Utilities/patches/fs/FsPatchMaker.java    | 192 ++++++++++++++++
 .../patches/fs/finders/HeuristicFsExFAT1.java | 179 +++++++++++++++
 .../patches/fs/finders/HeuristicFsExFAT2.java | 182 +++++++++++++++
 .../patches/fs/finders/HeuristicFsFAT1.java   | 176 +++++++++++++++
 .../patches/fs/finders/HeuristicFsFAT2.java   | 163 ++++++++++++++
 .../patches/fs/finders/HeuristicFsWizard.java | 139 ++++++++++++
 src/main/resources/PatchesTab.fxml            |   5 +-
 21 files changed, 1447 insertions(+), 86 deletions(-)
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/AHeuristic.java
 rename src/main/java/nsusbloader/Utilities/patches/{es => }/BinToAsmPrinter.java (92%)
 rename src/main/java/nsusbloader/Utilities/patches/{es => }/SimplyFind.java (99%)
 delete mode 100644 src/main/java/nsusbloader/Utilities/patches/es/finders/IHeuristicEs.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/FsNcaSearchTask.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/FsPatch.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/FsPatchMaker.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT1.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT2.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT1.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT2.java
 create mode 100644 src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsWizard.java

diff --git a/src/main/java/nsusbloader/Controllers/PatchesController.java b/src/main/java/nsusbloader/Controllers/PatchesController.java
index 66aaaf7..471bbe0 100644
--- a/src/main/java/nsusbloader/Controllers/PatchesController.java
+++ b/src/main/java/nsusbloader/Controllers/PatchesController.java
@@ -39,20 +39,21 @@ import nsusbloader.MediatorControl;
 import nsusbloader.NSLDataTypes.EModule;
 import nsusbloader.ServiceWindow;
 import nsusbloader.Utilities.patches.es.EsPatchMaker;
+import nsusbloader.Utilities.patches.fs.FsPatchMaker;
 
 // TODO: CLI SUPPORT
 public class PatchesController implements Initializable {
     @FXML
     private VBox patchesToolPane;
     @FXML
-    private Button selFwFolderBtn, selProdKeysBtn, makeEsBtn;
+    private Button selFwFolderBtn, selProdKeysBtn, makeEsBtn, makeFsBtn;
     @FXML
     private Label shortNameFirmwareLbl, locationFirmwareLbl, saveToLbl, shortNameKeysLbl, locationKeysLbl, statusLbl;
     private Thread workThread;
 
     private String previouslyOpenedPath;
     private ResourceBundle resourceBundle;
-    private Region convertRegion;
+    private Region convertRegionEs;
 
     @Override
     public void initialize(URL url, ResourceBundle resourceBundle) {
@@ -70,9 +71,10 @@ public class PatchesController implements Initializable {
         locationKeysLbl.textProperty().addListener((observableValue, currentText, updatedText) ->
                 shortNameKeysLbl.setText(updatedText.replaceAll(myRegexp, "")));
 
-        convertRegion = new Region();
-        convertRegion.getStyleClass().add("regionCake");
-        makeEsBtn.setGraphic(convertRegion);
+        convertRegionEs = new Region();
+        convertRegionEs.getStyleClass().add("regionCake");
+        makeEsBtn.setGraphic(convertRegionEs);
+        //makeFsBtn.setGraphic(convertRegionEs);
 
         AppPreferences preferences = AppPreferences.getInstance();
         String keysLocation = preferences.getKeysLocation();
@@ -85,6 +87,7 @@ public class PatchesController implements Initializable {
         saveToLbl.setText(preferences.getPatchesSaveToLocation());
         //makeEsBtn.disableProperty().bind(Bindings.isEmpty(locationFirmwareLbl.textProperty()));
         makeEsBtn.setOnAction(actionEvent -> makeEs());
+        makeFsBtn.setOnAction(actionEvent -> makeFs());
     }
 
     /**
@@ -174,6 +177,30 @@ public class PatchesController implements Initializable {
         workThread.setDaemon(true);
         workThread.start();
     }
+    private void makeFs(){
+        if (locationFirmwareLbl.getText().isEmpty() || locationKeysLbl.getText().isEmpty()){
+            ServiceWindow.getErrorNotification(resourceBundle.getString("windowTitleError"),
+                    resourceBundle.getString("tabPatches_ServiceWindowMessage"));
+            return;
+        }
+
+        if (workThread != null && workThread.isAlive())
+            return;
+        statusLbl.setText("");
+
+        if (MediatorControl.getInstance().getTransferActive()) {
+            ServiceWindow.getErrorNotification(resourceBundle.getString("windowTitleError"),
+                    resourceBundle.getString("windowBodyPleaseStopOtherProcessFirst"));
+            return;
+        }
+
+        FsPatchMaker fsPatchMaker = new FsPatchMaker(locationFirmwareLbl.getText(), locationKeysLbl.getText(),
+                saveToLbl.getText());
+        workThread = new Thread(fsPatchMaker);
+
+        workThread.setDaemon(true);
+        workThread.start();
+    }
     private void interruptProcessOfPatchMaking(){
         if (workThread == null || ! workThread.isAlive())
             return;
@@ -187,11 +214,11 @@ public class PatchesController implements Initializable {
             return;
         }
 
-        convertRegion.getStyleClass().clear();
+        convertRegionEs.getStyleClass().clear();
 
         if (isActive) {
             MediatorControl.getInstance().getContoller().logArea.clear();
-            convertRegion.getStyleClass().add("regionStop");
+            convertRegionEs.getStyleClass().add("regionStop");
 
             makeEsBtn.setOnAction(e-> interruptProcessOfPatchMaking());
             makeEsBtn.setText(resourceBundle.getString("btn_Stop"));
@@ -199,7 +226,7 @@ public class PatchesController implements Initializable {
             makeEsBtn.getStyleClass().add("buttonStop");
         }
         else {
-            convertRegion.getStyleClass().add("regionCake");
+            convertRegionEs.getStyleClass().add("regionCake");
 
             makeEsBtn.setOnAction(actionEvent -> makeEs());
             makeEsBtn.setText(resourceBundle.getString("tabPatches_Btn_MakeEs"));
diff --git a/src/main/java/nsusbloader/Utilities/patches/AHeuristic.java b/src/main/java/nsusbloader/Utilities/patches/AHeuristic.java
new file mode 100644
index 0000000..1d1b6ea
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/AHeuristic.java
@@ -0,0 +1,45 @@
+/*
+    Copyright 2018-2022 Dmitry Isaenko
+
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches;
+/**
+ * Searches instructions (via known patterns) that follows 'specific instruction' we want to patch.
+ * Returns offset of the pattern. Not offset of the 'specific instruction'.
+ * */
+public abstract class AHeuristic {
+    protected boolean isLDR(int expression){ return (expression >> 22 & 0x2FF) == 0x2e5; }// LDR ! Sounds like LDP, don't mess up
+    protected boolean isLDP(int expression){ return (expression >> 22 & 0x1F9) == 0xA1; }// LDP !
+    protected boolean isCBNZ(int expression){ return (expression >> 24 & 0x7f) == 0x35; }
+    protected boolean isMOV(int expression){ return (expression >> 23 & 0xff) == 0xA5; }
+    protected boolean isTBZ(int expression){ return (expression >> 24 & 0x7f) == 0x36; }
+    protected boolean isLDRB_LDURB(int expression){ return (expression >> 21 & 0x7f7) == 0x1c2; }
+    protected boolean isMOV_REG(int expression){ return (expression & 0x7FE0FFE0) == 0x2A0003E0; }
+    protected boolean isB(int expression) { return (expression >> 26 & 0x3f) == 0x5; }
+    protected boolean isBL(int expression){ return (expression >> 26 & 0x3f) == 0x25; }
+    protected boolean isADD(int expression){ return (expression >> 23 & 0xff) == 0x22; }
+    public abstract boolean isFound();
+    public abstract boolean wantLessEntropy();
+    public abstract int getOffset() throws Exception;
+    public abstract String getDetails();
+
+    /**
+     * Should be used if wantLessEntropy() == true
+     * @return isFound();
+     * */
+    public abstract boolean setOffsetsNearby(int offsetNearby);
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/BinToAsmPrinter.java b/src/main/java/nsusbloader/Utilities/patches/BinToAsmPrinter.java
similarity index 92%
rename from src/main/java/nsusbloader/Utilities/patches/es/BinToAsmPrinter.java
rename to src/main/java/nsusbloader/Utilities/patches/BinToAsmPrinter.java
index 65a8fed..36cb41f 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/BinToAsmPrinter.java
+++ b/src/main/java/nsusbloader/Utilities/patches/BinToAsmPrinter.java
@@ -16,10 +16,9 @@
     You should have received a copy of the GNU General Public License
     along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nsusbloader.Utilities.patches.es;
+package nsusbloader.Utilities.patches;
 
 import libKonogonka.Converter;
-import nsusbloader.Main;
 import nsusbloader.NSLMain;
 
 public class BinToAsmPrinter {
@@ -123,6 +122,12 @@ public class BinToAsmPrinter {
                 }
             case 0xA2:
                 return printSUBSimplified(instructionExpression, offset);
+            case 0xE2:
+            case 0x1e2:
+                return printCMPSimplified(instructionExpression, offset);
+            case 0x24:
+            case 0x124:
+                return printANDSimplified(instructionExpression, offset);
         }
 
         switch (instructionExpression >> 24 & 0xff) {
@@ -145,6 +150,7 @@ public class BinToAsmPrinter {
             case 0x25:
                 return printBLSimplified(instructionExpression, offset);
         }
+        System.out.printf("0x%x\n", (instructionExpression >> 23 & 0xff));
         return  printUnknownSimplified(instructionExpression, offset);
     }
 
@@ -411,7 +417,7 @@ public class BinToAsmPrinter {
         int conditionalJumpLocation = ((instructionExpression >> 4 & 0b1111111111111111111) * 4 + offset) & 0xfffff;
 
         return String.format(
-                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   B.%s           " + ANSI_BLUE + "#0x%x" + ANSI_RESET + " (Real: " + ANSI_BLUE + "#0x%x" + ANSI_RESET + ")\n",
+                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   B.%s        " + ANSI_BLUE + "#0x%x" + ANSI_RESET + " (Real: " + ANSI_BLUE + "#0x%x" + ANSI_RESET + ")\n",
                 offset, Integer.reverseBytes(instructionExpression), instructionExpression,
                 getBConditionalMarker(instructionExpression & 0xf),
                 conditionalJumpLocation, (conditionalJumpLocation + 0x100));
@@ -437,8 +443,8 @@ public class BinToAsmPrinter {
 
     private static String printMOVRegisterSimplified(int instructionExpression, int offset){    //ADD (immediate)
         String sfHw = (instructionExpression >> 31 & 1) == 0 ? "W" : "X";
-        int Rm = instructionExpression >> 16 & 0xF;
-        int Rd = instructionExpression & 0xF;
+        int Rm = instructionExpression >> 16 & 0x1F;
+        int Rd = instructionExpression & 0x1F;
 
         return String.format(
                 "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   MOV (reg)   " + ANSI_GREEN + "%s%d " + ANSI_BLUE + "%s%d" + ANSI_RESET + "\n",
@@ -480,11 +486,40 @@ public class BinToAsmPrinter {
                 wx, Rt, wx, Rn, imm12);
     }
 
+    private static String printCMPSimplified(int instructionExpression, int offset){
+        String sf = (instructionExpression >> 31 == 0) ? "W" : "X";
+        int Rn = instructionExpression >> 5 & 0x1F;
+        int conditionalJumpLocation = (instructionExpression >> 10) & 0xfff;
+        int LSL = (instructionExpression >> 22 & 0b1) == 1 ? 12 : 0;
 
+        return String.format(
+                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   CMP         " + ANSI_GREEN + sf + "%d," +
+                        ANSI_BLUE + "0x%x" + ANSI_RESET + " (Real: " + ANSI_BLUE + "#0x%x" + ANSI_RESET + ") " + ANSI_PURPLE + "LSL #%d" + ANSI_RESET + "\n",
+                offset, Integer.reverseBytes(instructionExpression), instructionExpression,
+                Rn,
+                conditionalJumpLocation, (conditionalJumpLocation + 0x100),
+                LSL);
+    }
+
+    private static String printANDSimplified(int instructionExpression, int offset){
+        String sf = (instructionExpression >> 31 == 0) ? "W" : "X";
+        int Rn = instructionExpression & 0x1F;
+        int Rd = instructionExpression >> 5 & 0x1F;
+        int imm;
+        if (sf.equals("W"))
+            imm = instructionExpression >> 10 & 0xfff;
+        else
+            imm = instructionExpression >> 10 & 0x1fff;
+
+        return String.format(
+                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   AND         " + ANSI_GREEN + sf + "%d, " + ANSI_BLUE +
+                        sf + "%d" + ANSI_PURPLE + " # ??? 0b%s " + ANSI_RESET + "\n",
+                offset, Integer.reverseBytes(instructionExpression), instructionExpression, Rn, Rd, Converter.intToBinaryString(imm));
+    }
 
     private static String printUnknownSimplified(int instructionExpression, int offset){
         return String.format(
-                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   ???          0b"+ANSI_RESET+ Converter.intToBinaryString(Integer.reverseBytes(instructionExpression)) +"\n",
+                "%05x "+ANSI_CYAN+"%08x (%08x)"+ANSI_YELLOW + "   ???          0b"+ANSI_RESET+ Converter.intToBinaryString(instructionExpression) +"\n",
                 offset, Integer.reverseBytes(instructionExpression), instructionExpression);
     }
 
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/SimplyFind.java b/src/main/java/nsusbloader/Utilities/patches/SimplyFind.java
similarity index 99%
rename from src/main/java/nsusbloader/Utilities/patches/es/SimplyFind.java
rename to src/main/java/nsusbloader/Utilities/patches/SimplyFind.java
index c3a648f..e16a824 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/SimplyFind.java
+++ b/src/main/java/nsusbloader/Utilities/patches/SimplyFind.java
@@ -16,7 +16,7 @@
     You should have received a copy of the GNU General Public License
     along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nsusbloader.Utilities.patches.es;
+package nsusbloader.Utilities.patches;
 
 import libKonogonka.Converter;
 
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/EsNcaSearchTask.java b/src/main/java/nsusbloader/Utilities/patches/es/EsNcaSearchTask.java
index 12edaff..36b9716 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/EsNcaSearchTask.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/EsNcaSearchTask.java
@@ -19,7 +19,7 @@
 package nsusbloader.Utilities.patches.es;
 
 import libKonogonka.Converter;
-import libKonogonka.Tools.NCA.NCAProvider;
+import libKonogonka.fs.NCA.NCAProvider;
 
 import java.util.List;
 import java.util.concurrent.Callable;
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/EsPatch.java b/src/main/java/nsusbloader/Utilities/patches/es/EsPatch.java
index e0b2003..f513c8b 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/EsPatch.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/EsPatch.java
@@ -22,11 +22,12 @@
 package nsusbloader.Utilities.patches.es;
 
 import libKonogonka.Converter;
-import libKonogonka.Tools.NCA.NCAProvider;
-import libKonogonka.Tools.NSO.NSO0Header;
-import libKonogonka.Tools.NSO.NSO0Provider;
+import libKonogonka.fs.NCA.NCAProvider;
+import libKonogonka.fs.NSO.NSO0Header;
+import libKonogonka.fs.NSO.NSO0Provider;
 import nsusbloader.ModelControllers.ILogPrinter;
 import nsusbloader.NSLDataTypes.EMsgType;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
 import nsusbloader.Utilities.patches.es.finders.HeuristicEsWizard;
 
 import java.io.BufferedOutputStream;
@@ -35,6 +36,7 @@ import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.Arrays;
 
 public class EsPatch {
@@ -113,7 +115,7 @@ public class EsPatch {
         handyEsPatch.put(getFooter());
 
         try (BufferedOutputStream stream = new BufferedOutputStream(
-                Files.newOutputStream(new File(patchFileLocation).toPath()))){
+                Files.newOutputStream(Paths.get(patchFileLocation)))){
             stream.write(handyEsPatch.array());
         }
         logPrinter.print("Patch created at "+patchFileLocation, EMsgType.PASS);
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/EsPatchMaker.java b/src/main/java/nsusbloader/Utilities/patches/es/EsPatchMaker.java
index b28e2ec..e7d85fd 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/EsPatchMaker.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/EsPatchMaker.java
@@ -19,7 +19,7 @@
 package nsusbloader.Utilities.patches.es;
 
 import libKonogonka.KeyChainHolder;
-import libKonogonka.Tools.NCA.NCAProvider;
+import libKonogonka.fs.NCA.NCAProvider;
 import nsusbloader.ModelControllers.CancellableRunnable;
 import nsusbloader.ModelControllers.ILogPrinter;
 import nsusbloader.ModelControllers.Log;
@@ -92,6 +92,8 @@ public class EsPatchMaker extends CancellableRunnable {
     private void receiveFirmware() throws Exception{
         logPrinter.print("Looking at firmware", EMsgType.INFO);
         this.firmware = new File(pathToFirmware);
+        if (! firmware.exists())
+            throw new Exception("Firmware directory does not exist " + pathToFirmware);
     }
     private void buildKeyChainHolder() throws Exception{
         logPrinter.print("Reading keys", EMsgType.INFO);
@@ -174,7 +176,7 @@ public class EsPatchMaker extends CancellableRunnable {
         subTasks.add(task);
         return subTasks;
     }
-    private List<NCAProvider>  getNextSet(Iterator<String> iterator, int amount) throws Exception{
+    private List<NCAProvider> getNextSet(Iterator<String> iterator, int amount) throws Exception{
         List<NCAProvider> ncas = new ArrayList<>();
         for (int j = 0; j < amount; j++){
             String ncaFileName = iterator.next();
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs1.java b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs1.java
index cfd1974..550d002 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs1.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs1.java
@@ -19,12 +19,13 @@
 package nsusbloader.Utilities.patches.es.finders;
 
 import libKonogonka.Converter;
-import nsusbloader.Utilities.patches.es.BinToAsmPrinter;
-import nsusbloader.Utilities.patches.es.SimplyFind;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
 
 import java.util.List;
 
-class HeuristicEs1 implements IHeuristicEs {
+class HeuristicEs1 extends AHeuristic {
     private static final String PATTERN = "1F90013128.8052";
 
     private final List<Integer> findings;
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs2.java b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs2.java
index b3fd297..4dba546 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs2.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs2.java
@@ -19,13 +19,14 @@
 package nsusbloader.Utilities.patches.es.finders;
 
 import libKonogonka.Converter;
-import nsusbloader.Utilities.patches.es.BinToAsmPrinter;
-import nsusbloader.Utilities.patches.es.SimplyFind;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
 
 import java.util.ArrayList;
 import java.util.List;
 
-class HeuristicEs2 implements IHeuristicEs {
+class HeuristicEs2 extends AHeuristic {
     private static final String PATTERN = ".D2.52";
 
     private List<Integer> findings;
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs3.java b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs3.java
index 8d7d5fd..728a98a 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs3.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEs3.java
@@ -19,12 +19,13 @@
 package nsusbloader.Utilities.patches.es.finders;
 
 import libKonogonka.Converter;
-import nsusbloader.Utilities.patches.es.BinToAsmPrinter;
-import nsusbloader.Utilities.patches.es.SimplyFind;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
 
 import java.util.List;
 
-class HeuristicEs3 implements IHeuristicEs {
+class HeuristicEs3 extends AHeuristic {
     private static final String PATTERN0 = "..FF97";
     private static final String PATTERN1 = "......FF97"; // aka "E0230091..FF97";
 
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEsWizard.java b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEsWizard.java
index 5afdf60..e130ca9 100644
--- a/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEsWizard.java
+++ b/src/main/java/nsusbloader/Utilities/patches/es/finders/HeuristicEsWizard.java
@@ -18,14 +18,16 @@
  */
 package nsusbloader.Utilities.patches.es.finders;
 
+import nsusbloader.Utilities.patches.AHeuristic;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
 public class HeuristicEsWizard {
-    private final List<IHeuristicEs> all;
-    private final List<IHeuristicEs> found;
-    private final List<IHeuristicEs> wantLessEntropy;
+    private final List<AHeuristic> all;
+    private final List<AHeuristic> found;
+    private final List<AHeuristic> wantLessEntropy;
 
     private final StringBuilder errorsAndNotes;
 
@@ -43,14 +45,14 @@ public class HeuristicEsWizard {
         );
 
         this.found = all.stream()
-                .filter(IHeuristicEs::isFound)
+                .filter(AHeuristic::isFound)
                 .collect(Collectors.toList());
 
         if (found.isEmpty())
             throw new Exception("Nothing found!");
 
         this.wantLessEntropy = all.stream()
-                .filter(IHeuristicEs::wantLessEntropy)
+                .filter(AHeuristic::wantLessEntropy)
                 .collect(Collectors.toList());
 
         shareOffsetsWithEachOther();
@@ -61,14 +63,14 @@ public class HeuristicEsWizard {
     }
 
     private void shareOffsetsWithEachOther(){
-        for (IHeuristicEs es : wantLessEntropy) {
+        for (AHeuristic es : wantLessEntropy) {
             if (shareWithNext(es))
                 return;
         }
     }
-    private boolean shareWithNext(IHeuristicEs es){
+    private boolean shareWithNext(AHeuristic es){
         try {
-            for (IHeuristicEs foundEs : found) {
+            for (AHeuristic foundEs : found) {
                 if (es.setOffsetsNearby(foundEs.getOffset())) {
                     found.add(es);
                     wantLessEntropy.remove(es);
diff --git a/src/main/java/nsusbloader/Utilities/patches/es/finders/IHeuristicEs.java b/src/main/java/nsusbloader/Utilities/patches/es/finders/IHeuristicEs.java
deleted file mode 100644
index 944b474..0000000
--- a/src/main/java/nsusbloader/Utilities/patches/es/finders/IHeuristicEs.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-    Copyright 2018-2022 Dmitry Isaenko
-
-    This file is part of NS-USBloader.
-
-    NS-USBloader is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    NS-USBloader is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nsusbloader.Utilities.patches.es.finders;
-/**
- * Searches instructions (via known patterns) that follows 'specific instruction' we want to patch.
- * Returns offset of the pattern. Not offset of the 'specific instruction'.
- * */
-interface IHeuristicEs {
-    default boolean isLDR(int expression){ return (expression >> 22 & 0x2FF) == 0x2e5; }// LDR ! Sounds like LDP, don't mess up
-    default boolean isLDP(int expression){ return (expression >> 22 & 0x1F9) == 0xA1; }// LDP !
-    default boolean isCBNZ(int expression){ return (expression >> 24 & 0x7f) == 0x35; }
-    default boolean isMOV(int expression){ return (expression >> 23 & 0xff) == 0xA5; }
-    default boolean isTBZ(int expression){ return (expression >> 24 & 0x7f) == 0x36; }
-    default boolean isLDRB_LDURB(int expression){ return (expression >> 21 & 0x7f7) == 0x1c2; }
-    default boolean isMOV_REG(int expression){ return (expression & 0x7FE0FFE0) == 0x2A0003E0; }
-    default boolean isB(int expression) { return (expression >> 26 & 0x3f) == 0x5; }
-    default boolean isBL(int expression){ return (expression >> 26 & 0x3f) == 0x25; }
-    default boolean isADD(int expression){ return (expression >> 23 & 0xff) == 0x22; }
-    boolean isFound();
-    boolean wantLessEntropy();
-    int getOffset() throws Exception;
-    String getDetails();
-
-    /**
-     * Should be used if wantLessEntropy() == true
-     * @return isFound();
-     * */
-    boolean setOffsetsNearby(int offsetNearby);
-}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/FsNcaSearchTask.java b/src/main/java/nsusbloader/Utilities/patches/fs/FsNcaSearchTask.java
new file mode 100644
index 0000000..718a234
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/FsNcaSearchTask.java
@@ -0,0 +1,50 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+     
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs;
+
+import libKonogonka.Converter;
+import libKonogonka.fs.NCA.NCAProvider;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+class FsNcaSearchTask implements Callable<NCAProvider> {
+    private final List<NCAProvider> ncaProviders;
+
+    FsNcaSearchTask(List<NCAProvider> ncaProviders){
+        this.ncaProviders = ncaProviders;
+    }
+
+    @Override
+    public NCAProvider call() {
+        try {
+            for (NCAProvider ncaProvider : ncaProviders) {
+                String titleId = Converter.byteArrToHexStringAsLE(ncaProvider.getTitleId());
+                if (titleId.equals("0100000000000819") || titleId.equals("010000000000081b")) { // eq. FAT || exFAT
+                    return ncaProvider;
+                }
+            }
+            return null;
+        }
+        catch (Exception e){
+            e.printStackTrace();
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/FsPatch.java b/src/main/java/nsusbloader/Utilities/patches/fs/FsPatch.java
new file mode 100644
index 0000000..84649ce
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/FsPatch.java
@@ -0,0 +1,208 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+     
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+    ---
+    Based on FS-AutoIPS.py patch script made by GBATemp member MrDude.
+    Taken from: https://gbatemp.net/threads/info-on-sha-256-hashes-on-fs-patches.581550/
+ */
+package nsusbloader.Utilities.patches.fs;
+
+import libKonogonka.Converter;
+import libKonogonka.KeyChainHolder;
+import libKonogonka.RainbowDump;
+import libKonogonka.fs.NCA.NCAProvider;
+import libKonogonka.fs.NSO.NSO0Provider;
+import libKonogonka.fs.RomFs.FileSystemEntry;
+import libKonogonka.fs.RomFs.RomFsProvider;
+import libKonogonka.fs.other.System2.System2Provider;
+import libKonogonka.fs.other.System2.ini1.Ini1Provider;
+import libKonogonka.fs.other.System2.ini1.KIP1Provider;
+import libKonogonka.aesctr.InFileStreamProducer;
+import nsusbloader.ModelControllers.ILogPrinter;
+import nsusbloader.NSLDataTypes.EMsgType;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.fs.finders.HeuristicFsWizard;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+public class FsPatch {
+    private final NCAProvider ncaProvider;
+    private final String saveToLocation;
+    private final KeyChainHolder keyChainHolder;
+    private final ILogPrinter logPrinter;
+
+    private long fwVersion;
+    private String patchName;
+    private byte[] _textSection;
+    private boolean filesystemTypeFat32;
+
+    private HeuristicFsWizard wizard;
+
+    FsPatch(NCAProvider ncaProvider, String saveToLocation, KeyChainHolder keyChainHolder, ILogPrinter logPrinter) throws Exception{
+        this.ncaProvider = ncaProvider;
+        this.saveToLocation = saveToLocation + File.separator +
+                "atmosphere" + File.separator + "kip_patches" + File.separator + "fs_patches";
+        this.keyChainHolder = keyChainHolder;
+        this.logPrinter = logPrinter;
+
+        KIP1Provider kip1Provider = getKIP1Provider();
+        getPatchName(kip1Provider);
+        getTextSection(kip1Provider);
+        checkFirmwareVersion();
+        getFilesystemType();
+        findAllOffsets();
+        //mkDirs();
+        //writeFile();
+        //updatePatchesIni();
+        //logPrinter.print("                  == Debug information ==\n"+wizard.getDebug(), EMsgType.NULL);
+    }
+    private KIP1Provider getKIP1Provider() throws Exception{
+        RomFsProvider romFsProvider = ncaProvider.getNCAContentProvider(0).getRomfs();
+
+        FileSystemEntry package2FSEntry = romFsProvider.getRootEntry().getContent()
+                .stream()
+                .filter(e -> e.getName().equals("nx"))
+                .collect(Collectors.toList())
+                .get(0)
+                .getContent()
+                .stream()
+                .filter(e -> e.getName().equals("package2"))
+                .collect(Collectors.toList())
+                .get(0);
+        InFileStreamProducer producer = romFsProvider.getStreamProducer(package2FSEntry);
+        System2Provider system2Provider = new System2Provider(producer, keyChainHolder);
+        Ini1Provider ini1Provider = system2Provider.getIni1Provider();
+
+        KIP1Provider kip1Provider = ini1Provider.getKip1List().stream()
+                .filter(provider -> provider.getHeader().getName().startsWith("FS"))
+                .collect(Collectors.toList())
+                .get(0);
+
+        if (kip1Provider == null)
+            throw new Exception("No FS KIP1");
+        return kip1Provider;
+    }
+    private void getPatchName(KIP1Provider kip1Provider) throws Exception{
+        int kip1EncryptedSize = (int) kip1Provider.getSize();
+        byte[] kip1EncryptedRaw = new byte[kip1EncryptedSize];
+
+        try (BufferedInputStream kip1ProviderStream = kip1Provider.getStreamProducer().produce()) {
+            if (kip1EncryptedSize != kip1ProviderStream.read(kip1EncryptedRaw))
+                throw new Exception("Unencrypted FS KIP1 read failure");
+        }
+
+        byte[] sha256ofKip1 = MessageDigest.getInstance("SHA-256").digest(kip1EncryptedRaw);
+        patchName = Converter.byteArrToHexStringAsLE(sha256ofKip1, true) + ".ips";
+    }
+    private void getTextSection(KIP1Provider kip1Provider) throws Exception{
+        _textSection = kip1Provider.getAsDecompressed().getTextRaw();
+    }
+    private void checkFirmwareVersion() throws Exception{
+        final byte[] byteSdkVersion = ncaProvider.getSdkVersion();
+        fwVersion = Long.parseLong(""+byteSdkVersion[3] + byteSdkVersion[2] + byteSdkVersion[1] + byteSdkVersion[0]);
+        logPrinter.print("Internal firmware version: " +
+                byteSdkVersion[3] +"."+byteSdkVersion[2] +"."+byteSdkVersion[1] +"."+byteSdkVersion[0], EMsgType.INFO);
+        System.out.println("FW "+byteSdkVersion[3] +"."+byteSdkVersion[2] +"."+byteSdkVersion[1] +"."+byteSdkVersion[0]);                           // TODO:REMOVE!
+        if (fwVersion < 9300)
+            logPrinter.print("WARNING! FIRMWARES VERSIONS BEFORE 9.0.0 ARE NOT SUPPORTED! " +
+                    "USING PRODUCED ES PATCHES (IF ANY) COULD BREAK SOMETHING! IT'S NEVER BEEN TESTED!", EMsgType.WARNING);
+    }
+    private void getFilesystemType(){
+        String titleId = Converter.byteArrToHexStringAsLE(ncaProvider.getTitleId());
+        filesystemTypeFat32 = titleId.equals("0100000000000819");
+    }
+    private void findAllOffsets() throws Exception{
+        // TODO: FIX, IMPLEMENT, DEPLOY
+        this.wizard = new HeuristicFsWizard(fwVersion, _textSection, filesystemTypeFat32);
+        String errorsAndNotes = wizard.getErrorsAndNotes();
+        if (errorsAndNotes.length() > 0)
+            logPrinter.print(errorsAndNotes, EMsgType.WARNING);
+    }
+    private void mkDirs(){
+        File parentFolder = new File(saveToLocation);
+        parentFolder.mkdirs();
+    }
+
+    private void writeFile() throws Exception{
+        String patchFileLocation = saveToLocation + File.separator + patchName; // THIS IS GOOD
+        int offset1 = wizard.getOffset1();
+
+        ByteBuffer handyFsPatch = ByteBuffer.allocate(0x23).order(ByteOrder.LITTLE_ENDIAN);
+        handyFsPatch.put(getHeader());
+        // TODO: FIX, UPDATE
+        if (offset1 > 0) {
+            logPrinter.print("Patch component 1 will be used", EMsgType.PASS);
+            handyFsPatch.put(getPatch1(offset1));
+        }
+        handyFsPatch.put(getFooter());
+
+        try (BufferedOutputStream stream = new BufferedOutputStream(
+                Files.newOutputStream(Paths.get(patchFileLocation)))){
+            stream.write(handyFsPatch.array());
+        }
+        logPrinter.print("Patch created at "+patchFileLocation, EMsgType.PASS);
+    }
+    private byte[] getHeader(){
+        return "PATCH".getBytes(StandardCharsets.US_ASCII);
+    }
+    private byte[] getFooter(){
+        return "EOF".getBytes(StandardCharsets.US_ASCII);
+    }
+
+    private byte[] getPatch1(int offset) throws Exception{
+        int requiredInstructionOffsetInternal = offset - 4;
+        int requiredInstructionOffsetReal = requiredInstructionOffsetInternal + 0x100;
+        int instructionExpression = Converter.getLEint(_textSection, requiredInstructionOffsetInternal);
+        int patch = ((0x14 << 24) | (instructionExpression >> 5) & 0x7FFFF);
+
+        logPrinter.print(BinToAsmPrinter.printSimplified(patch, requiredInstructionOffsetInternal), EMsgType.NULL);
+
+        // Somehow IPS patches uses offsets written as big_endian (0.o) and bytes dat should be patched as LE.
+        ByteBuffer prePatch = ByteBuffer.allocate(10).order(ByteOrder.BIG_ENDIAN)
+                .putInt(requiredInstructionOffsetReal)
+                .putShort((short) 4)
+                .putInt(Integer.reverseBytes(patch));
+
+        return Arrays.copyOfRange(prePatch.array(), 1, 10);
+    }
+    private byte[] getPatch2(int offset) throws Exception{
+        int requiredInstructionOffsetInternal = offset - 4;
+        int requiredInstructionOffsetReal = requiredInstructionOffsetInternal + 0x100;
+        int instructionExpression = Converter.getLEint(_textSection, requiredInstructionOffsetInternal);
+        int patch = ((0x14 << 24) | (instructionExpression >> 5) & 0x7FFFF);
+
+        logPrinter.print(BinToAsmPrinter.printSimplified(patch, requiredInstructionOffsetInternal), EMsgType.NULL);
+
+        // Somehow IPS patches uses offsets written as big_endian (0.o) and bytes dat should be patched as LE.
+        ByteBuffer prePatch = ByteBuffer.allocate(10).order(ByteOrder.BIG_ENDIAN)
+                .putInt(requiredInstructionOffsetReal)
+                .putShort((short) 4)
+                .putInt(Integer.reverseBytes(patch));
+
+        return Arrays.copyOfRange(prePatch.array(), 1, 10);
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/FsPatchMaker.java b/src/main/java/nsusbloader/Utilities/patches/fs/FsPatchMaker.java
new file mode 100644
index 0000000..f33c4fa
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/FsPatchMaker.java
@@ -0,0 +1,192 @@
+/*
+    Copyright 2018-2022 Dmitry Isaenko
+     
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs;
+
+import libKonogonka.KeyChainHolder;
+import libKonogonka.fs.NCA.NCAProvider;
+import nsusbloader.ModelControllers.CancellableRunnable;
+import nsusbloader.ModelControllers.ILogPrinter;
+import nsusbloader.NSLDataTypes.EMsgType;
+import nsusbloader.NSLDataTypes.EFileStatus;
+
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.*;
+
+public class FsPatchMaker extends CancellableRunnable {
+    private int THREADS_POOL_SIZE = 4;
+    private final ILogPrinter logPrinter;
+    private final String pathToFirmware;
+    private final String pathToKeysFile;
+    private final String saveTo;
+
+    private File firmware;
+    private KeyChainHolder keyChainHolder;
+    private ExecutorService executorService;
+    private List<String> ncaFilesList; // inside the folder
+
+    private boolean oneLinerStatus = false;
+
+    public FsPatchMaker(String pathToFirmware, String pathToKeysFile, String saveTo){
+        //this.logPrinter = Log.getPrinter(EModule.PATCHES); //TODO: UNCOMMENT
+
+        this.logPrinter = new ILogPrinter() {
+            @Override
+            public void print(String message, EMsgType type) throws InterruptedException {}
+            @Override
+            public void updateProgress(Double value) throws InterruptedException {}
+            @Override
+            public void update(HashMap<String, File> nspMap, EFileStatus status) {}
+            @Override
+            public void update(File file, EFileStatus status) {}
+            @Override
+            public void updateOneLinerStatus(boolean status) {}
+            @Override
+            public void close() {}
+        };
+        // */
+        this.pathToFirmware = pathToFirmware;
+        this.pathToKeysFile = pathToKeysFile;
+        this.saveTo = saveTo;
+    }
+
+    @Override
+    public void run() {
+        try {
+            logPrinter.print("..:: Make FS Patches ::..", EMsgType.INFO);
+            receiveFirmware();
+            buildKeyChainHolder();
+            receiveNcaFileNamesList();
+            adjustThreadsPoolSize();
+            createPool();
+            executePool();
+        }
+        catch (Exception e){
+            e.printStackTrace();
+            try{
+                logPrinter.print(e.getMessage(), EMsgType.FAIL);
+            } catch (Exception ignore){}
+        }
+        finally {
+            logPrinter.updateOneLinerStatus(oneLinerStatus);
+            logPrinter.close();
+        }
+    }
+    private void receiveFirmware() throws Exception{
+        logPrinter.print("Looking at firmware", EMsgType.INFO);
+        this.firmware = new File(pathToFirmware);
+        if (! firmware.exists())
+            throw new Exception("Firmware directory does not exist " + pathToFirmware);
+    }
+    private void buildKeyChainHolder() throws Exception{
+        logPrinter.print("Reading keys", EMsgType.INFO);
+        this.keyChainHolder = new KeyChainHolder(pathToKeysFile, null);
+    }
+    private void receiveNcaFileNamesList() throws Exception{
+        logPrinter.print("Collecting NCA files", EMsgType.INFO);
+        String[] fileNamesArray = firmware.list(
+                (File directory, String file) -> ( ! file.endsWith(".cnmt.nca") && file.endsWith(".nca")));
+        ncaFilesList = Arrays.asList(Objects.requireNonNull(fileNamesArray));
+        if (ncaFilesList.size() == 0)
+            throw new Exception("No NCA files found in firmware folder");
+    }
+    private void adjustThreadsPoolSize(){
+        if (ncaFilesList.size() < 4)
+            THREADS_POOL_SIZE = ncaFilesList.size();
+    }
+
+    private void createPool() throws Exception{
+        logPrinter.print("Creating sub-tasks pool", EMsgType.INFO);
+        this.executorService = Executors.newFixedThreadPool(
+                THREADS_POOL_SIZE,
+                runnable -> {
+                    Thread thread = new Thread(runnable);
+                    thread.setDaemon(true);
+                    return thread;
+                });
+    }
+
+    private void executePool() throws Exception{ //TODO: FIX. Exceptions thrown only by logPrinter
+        try {
+            logPrinter.print("Executing sub-tasks pool", EMsgType.INFO);
+            List<Future<NCAProvider>> futuresResults = executorService.invokeAll(getSubTasksCollection());
+            int counter = 0;
+            for (Future<NCAProvider> future : futuresResults){
+                NCAProvider ncaProvider = future.get();
+                if (ncaProvider != null) {
+                    makePatches(ncaProvider);
+                    if (++counter > 1)
+                        break;
+                }
+            }
+            executorService.shutdown();
+        }
+        catch (InterruptedException ie){
+            executorService.shutdownNow();
+            boolean interruptedSuccessfully = false;
+            try {
+                interruptedSuccessfully = executorService.awaitTermination(20, TimeUnit.SECONDS);
+            }
+            catch (InterruptedException awaitInterrupt){
+                logPrinter.print("Force interrupting task...", EMsgType.WARNING);
+            }
+            logPrinter.print("Task interrupted "+(interruptedSuccessfully?"successfully":"with some issues"), EMsgType.WARNING);
+        }
+        catch (Exception e){
+            e.printStackTrace();
+            logPrinter.print("Task failed: "+e.getMessage(), EMsgType.FAIL);
+        }
+    }
+
+    private void makePatches(NCAProvider ncaProvider) throws Exception{
+        logPrinter.print(String.format("File found: .."+File.separator+"%s"+File.separator+"%s",
+                        ncaProvider.getFile().getParentFile().getName(), ncaProvider.getFile().getName())
+                , EMsgType.INFO);
+        //TODO : FIX; IMPLEMENT; DEPLOY ;)
+        new FsPatch(ncaProvider, saveTo, keyChainHolder, logPrinter);
+        oneLinerStatus = true;
+    }
+    private List<Callable<NCAProvider>> getSubTasksCollection() throws Exception{
+        logPrinter.print("Forming sub-tasks collection", EMsgType.INFO);
+        List<Callable<NCAProvider>> subTasks = new ArrayList<>();
+
+        int ncaPerThreadAmount = ncaFilesList.size() / THREADS_POOL_SIZE;
+        Iterator<String> iterator = ncaFilesList.listIterator();
+
+        for (int i = 1; i < THREADS_POOL_SIZE; i++){
+            Callable<NCAProvider> task = new FsNcaSearchTask(getNextSet(iterator, ncaPerThreadAmount));
+            subTasks.add(task);
+        }
+
+        Callable<NCAProvider> task = new FsNcaSearchTask(getNextSet(iterator,
+                ncaFilesList.size() % THREADS_POOL_SIZE == 0 ? ncaPerThreadAmount : ncaPerThreadAmount+1));
+        subTasks.add(task);
+        return subTasks;
+    }
+    private List<NCAProvider> getNextSet(Iterator<String> iterator, int amount) throws Exception{
+        List<NCAProvider> ncas = new ArrayList<>();
+        for (int j = 0; j < amount; j++){
+            String ncaFileName = iterator.next();
+            File nca = new File(firmware.getAbsolutePath()+File.separator+ncaFileName);
+            NCAProvider provider = new NCAProvider(nca, keyChainHolder.getRawKeySet());
+            ncas.add(provider);
+        }
+        return ncas;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT1.java b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT1.java
new file mode 100644
index 0000000..148d3e5
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT1.java
@@ -0,0 +1,179 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs.finders;
+
+import libKonogonka.Converter;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
+
+import java.util.List;
+
+class HeuristicFsExFAT1 extends AHeuristic {
+    private static final String PATTERN0 = ".1e42b91fc14271";
+    private static final String PATTERN1 = "9408...1F05.....54";
+
+    private final List<Integer> findings;
+    private final byte[] where;
+
+    HeuristicFsExFAT1(long fwVersion, byte[] where){
+        this.where = where;
+        String pattern = getPattern(fwVersion);
+        SimplyFind simplyfind = new SimplyFind(pattern, where);
+        this.findings = simplyfind.getResults();
+        for (Integer find : findings)
+            System.out.println(getDetails(find));
+/*                                                                                                  FIXME
+        this.findings.removeIf(this::dropStep1);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep2);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep3);
+ */
+    }
+    private String getPattern(long fwVersion){
+        if (fwVersion < 15300) // & fwVersion >= 9300
+            return PATTERN0;
+        return PATTERN1;
+    }
+    // Let's focus on CBZ-ONLY statements
+    private boolean dropStep1(int offsetOfPatternFound){
+        return ((where[offsetOfPatternFound - 1] & (byte) 0b01111111) != 0x34);
+    }
+
+    private boolean dropStep2(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound - 4);
+
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation);
+        int afterJumpThirdExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        // Check first is 'MOV'; second is 'B'
+        return (! isMOV_REG(afterJumpSecondExpressions)) || ! isB(afterJumpThirdExpressions);
+    }
+
+    private boolean dropStep3(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound-4);
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        int secondPairConditionalJumpLocation = ((afterJumpSecondExpressions & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+        int thirdExpressionsPairElement1 = Converter.getLEint(where, secondPairConditionalJumpLocation);
+        int thirdExpressionsPairElement2 = Converter.getLEint(where, secondPairConditionalJumpLocation+4);
+        // Check first is 'ADD'; second is 'BL'
+        return (! isADD(thirdExpressionsPairElement1)) || (! isBL(thirdExpressionsPairElement2));
+    }
+
+    private int getCBZConditionalJumpLocation(int cbzOffsetInternal){
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        return ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+    }
+
+    @Override
+    public boolean isFound(){
+        return findings.size() == 1;
+    }
+
+    @Override
+    public boolean wantLessEntropy(){
+        return findings.size() > 1;
+    }
+
+    @Override
+    public int getOffset() throws Exception{
+        if(findings.isEmpty())
+            throw new Exception("Nothing found");
+        if (findings.size() > 1)
+            throw new Exception("Too many offsets");
+        return findings.get(0);
+    }
+
+    @Override
+    public boolean setOffsetsNearby(int offsetNearby) {
+        findings.removeIf(offset -> {
+            if (offset > offsetNearby)
+                return ! (offset < offsetNearby - 0xffff);
+            return ! (offset > offsetNearby - 0xffff);
+        });
+        return isFound();
+    }
+
+    public String getDetails(Integer value){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = value - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        if (((secondExpressionsPairElement2 >> 26 & 0b111111) == 0x5)){
+            builder.append("...\n");
+            int conditionalJumpLocation2 = ((secondExpressionsPairElement2 & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2), conditionalJumpLocation2));
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2+4), conditionalJumpLocation2+4));
+
+        }
+        else {
+            builder.append("NO CONDITIONAL JUMP ON 2nd iteration (HeuristicEs3)");
+        }
+        return builder.toString();
+    }
+    @Override
+    public String getDetails(){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = findings.get(0) - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        if (((secondExpressionsPairElement2 >> 26 & 0b111111) == 0x5)){
+            builder.append("...\n");
+            int conditionalJumpLocation2 = ((secondExpressionsPairElement2 & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2), conditionalJumpLocation2));
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2+4), conditionalJumpLocation2+4));
+
+        }
+        else {
+            builder.append("NO CONDITIONAL JUMP ON 2nd iteration (HeuristicEs3)");
+        }
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT2.java b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT2.java
new file mode 100644
index 0000000..5463a6e
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsExFAT2.java
@@ -0,0 +1,182 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs.finders;
+
+import libKonogonka.Converter;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
+
+import java.util.List;
+
+class HeuristicFsExFAT2 extends AHeuristic {
+    private static final String PATTERN0 = ".94081C00121F05007181000054";
+    private static final String PATTERN1 = "003688...1F";
+
+    private final List<Integer> findings;
+    private final byte[] where;
+
+    HeuristicFsExFAT2(long fwVersion, byte[] where){
+        this.where = where;
+        String pattern = getPattern(fwVersion);
+        SimplyFind simplyfind = new SimplyFind(pattern, where);
+        this.findings = simplyfind.getResults();
+
+        for (Integer find : findings)
+            System.out.println(getDetails(find));
+        /*                                                                                                  FIXME
+        this.findings.removeIf(this::dropStep1);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep2);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep3);
+
+ */
+    }
+    private String getPattern(long fwVersion){
+        if (fwVersion < 15300) // & fwVersion >= 9300
+            return PATTERN0;
+        return PATTERN1;
+    }
+    // Let's focus on CBZ-ONLY statements
+    private boolean dropStep1(int offsetOfPatternFound){
+        return ((where[offsetOfPatternFound - 1] & (byte) 0b01111111) != 0x34);
+    }
+
+    private boolean dropStep2(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound - 4);
+
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation);
+        int afterJumpThirdExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        // Check first is 'MOV'; second is 'B'
+        return (! isMOV_REG(afterJumpSecondExpressions)) || ! isB(afterJumpThirdExpressions);
+    }
+
+    private boolean dropStep3(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound-4);
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        int secondPairConditionalJumpLocation = ((afterJumpSecondExpressions & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+        int thirdExpressionsPairElement1 = Converter.getLEint(where, secondPairConditionalJumpLocation);
+        int thirdExpressionsPairElement2 = Converter.getLEint(where, secondPairConditionalJumpLocation+4);
+        // Check first is 'ADD'; second is 'BL'
+        return (! isADD(thirdExpressionsPairElement1)) || (! isBL(thirdExpressionsPairElement2));
+    }
+
+    private int getCBZConditionalJumpLocation(int cbzOffsetInternal){
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        return ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+    }
+
+    @Override
+    public boolean isFound(){
+        return findings.size() == 1;
+    }
+
+    @Override
+    public boolean wantLessEntropy(){
+        return findings.size() > 1;
+    }
+
+    @Override
+    public int getOffset() throws Exception{
+        if(findings.isEmpty())
+            throw new Exception("Nothing found");
+        if (findings.size() > 1)
+            throw new Exception("Too many offsets");
+        return findings.get(0);
+    }
+
+    @Override
+    public boolean setOffsetsNearby(int offsetNearby) {
+        findings.removeIf(offset -> {
+            if (offset > offsetNearby)
+                return ! (offset < offsetNearby - 0xffff);
+            return ! (offset > offsetNearby - 0xffff);
+        });
+        return isFound();
+    }
+
+    public String getDetails(Integer value){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = value - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        if (((secondExpressionsPairElement2 >> 26 & 0b111111) == 0x5)){
+            builder.append("...\n");
+            int conditionalJumpLocation2 = ((secondExpressionsPairElement2 & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2), conditionalJumpLocation2));
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2+4), conditionalJumpLocation2+4));
+
+        }
+        else {
+            builder.append("NO CONDITIONAL JUMP ON 2nd iteration (HeuristicEs3)");
+        }
+        return builder.toString();
+    }
+
+    @Override
+    public String getDetails(){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = findings.get(0) - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        if (((secondExpressionsPairElement2 >> 26 & 0b111111) == 0x5)){
+            builder.append("...\n");
+            int conditionalJumpLocation2 = ((secondExpressionsPairElement2 & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2), conditionalJumpLocation2));
+            builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, conditionalJumpLocation2+4), conditionalJumpLocation2+4));
+
+        }
+        else {
+            builder.append("NO CONDITIONAL JUMP ON 2nd iteration (HeuristicEs3)");
+        }
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT1.java b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT1.java
new file mode 100644
index 0000000..bae72b3
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT1.java
@@ -0,0 +1,176 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs.finders;
+
+import libKonogonka.Converter;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class HeuristicFsFAT1 extends AHeuristic {
+    private static final String PATTERN0 = ".1e42b91fc14271";
+    private static final String PATTERN1 = "...9408...1F05.....54"; // ...94 081C0012 1F050071 4101
+/*
+    710006eba0  c0  02  40  f9     ldr       x0 , [ x22 ]
+    710006eba4  5c  c9  02  94     bl        FUN_7100121114                             undefined FUN_7100121114()
+    710006eba8  08  1c  00  12     and       w8 , w0 , # 0xff
+ */
+    private final List<Integer> findings;
+    private final byte[] where;
+
+    HeuristicFsFAT1(long fwVersion, byte[] where){
+        this.where = where;
+        String pattern = getPattern(fwVersion);
+        SimplyFind simplyfind = new SimplyFind(pattern, where);
+        List<Integer> temporary = simplyfind.getResults();
+        if (fwVersion >= 15300){
+            this.findings = new ArrayList<>();
+            temporary.forEach(var -> findings.add(var + 4));
+        }
+        else
+            findings = temporary;
+
+        System.out.println("\t\tFAT32 # 1 +++++++++++++++++++++++++++++++");
+        for (Integer find : findings) {
+            System.out.println(getDetails(find));
+            System.out.println("------------------------------------------------------------------");
+        }
+/*                                                                                                  FIXME
+        this.findings.removeIf(this::dropStep1);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep2);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep3);
+ */
+    }
+    private String getPattern(long fwVersion){
+        if (fwVersion < 15300) // & fwVersion >= 9300
+            return PATTERN0;
+        return PATTERN1;
+    }
+    // Let's focus on CBZ-ONLY statements
+    private boolean dropStep1(int offsetOfPatternFound){
+        return ((where[offsetOfPatternFound - 1] & (byte) 0b01111111) != 0x34);
+    }
+
+    private boolean dropStep2(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound - 4);
+
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation);
+        int afterJumpThirdExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        // Check first is 'MOV'; second is 'B'
+        return (! isMOV_REG(afterJumpSecondExpressions)) || ! isB(afterJumpThirdExpressions);
+    }
+
+    private boolean dropStep3(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound-4);
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        int secondPairConditionalJumpLocation = ((afterJumpSecondExpressions & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+        int thirdExpressionsPairElement1 = Converter.getLEint(where, secondPairConditionalJumpLocation);
+        int thirdExpressionsPairElement2 = Converter.getLEint(where, secondPairConditionalJumpLocation+4);
+        // Check first is 'ADD'; second is 'BL'
+        return (! isADD(thirdExpressionsPairElement1)) || (! isBL(thirdExpressionsPairElement2));
+    }
+
+    private int getCBZConditionalJumpLocation(int cbzOffsetInternal){
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        return ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+    }
+
+    @Override
+    public boolean isFound(){
+        return findings.size() == 1;
+    }
+
+    @Override
+    public boolean wantLessEntropy(){
+        return findings.size() > 1;
+    }
+
+    @Override
+    public int getOffset() throws Exception{
+        if(findings.isEmpty())
+            throw new Exception("Nothing found");
+        if (findings.size() > 1)
+            throw new Exception("Too many offsets");
+        return findings.get(0);
+    }
+
+    @Override
+    public boolean setOffsetsNearby(int offsetNearby) {
+        findings.removeIf(offset -> {
+            if (offset > offsetNearby)
+                return ! (offset < offsetNearby - 0xffff);
+            return ! (offset > offsetNearby - 0xffff);
+        });
+        return isFound();
+    }
+
+    public String getDetails(Integer value){
+        int firstOffsetInternal = value - 4;
+        int firstExpression = Converter.getLEint(where, firstOffsetInternal);
+        int conditionalJumpLocation = ((firstExpression >> 5 & 0x7FFFF) * 4 + firstOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        StringBuilder builder = new StringBuilder();
+        //builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal-4*11), firstOffsetInternal-4*11));
+        //builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal-4*10), firstOffsetInternal-4*10));
+        //builder.append("^ ^ ^ ...\n");
+        builder.append(BinToAsmPrinter.printSimplified(firstExpression, firstOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal+4), firstOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal+8), firstOffsetInternal+8));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal+12), firstOffsetInternal+12));
+        builder.append("...\n");
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        return builder.toString();
+    }
+    @Override
+    public String getDetails(){
+        int firstOffsetInternal = findings.get(0) - 4;
+        int firstExpression = Converter.getLEint(where, firstOffsetInternal);
+        int conditionalJumpLocation = ((firstExpression >> 5 & 0x7FFFF) * 4 + firstOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        return BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal - 4 * 11), firstOffsetInternal - 4 * 11) +
+                BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal - 4 * 10), firstOffsetInternal - 4 * 10) +
+                "^ ^ ^ ...\n" +
+                BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal - 4 * 2), firstOffsetInternal - 4 * 2) +
+                "^ ^ ^ ...\n" +
+                BinToAsmPrinter.printSimplified(firstExpression, firstOffsetInternal) +
+                BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal + 4), firstOffsetInternal + 4) +
+                BinToAsmPrinter.printSimplified(Converter.getLEint(where, firstOffsetInternal + 8), firstOffsetInternal + 8) +
+                "...\n" +
+                BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation) +
+                BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation + 4);
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT2.java b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT2.java
new file mode 100644
index 0000000..c265a31
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsFAT2.java
@@ -0,0 +1,163 @@
+/*
+    Copyright 2018-2023 Dmitry Isaenko
+
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs.finders;
+
+import libKonogonka.Converter;
+import nsusbloader.Utilities.patches.AHeuristic;
+import nsusbloader.Utilities.patches.BinToAsmPrinter;
+import nsusbloader.Utilities.patches.SimplyFind;
+
+import java.util.List;
+
+class HeuristicFsFAT2 extends AHeuristic {
+    private static final String PATTERN0 = "...94081C00121F05007181000054";
+    private static final String PATTERN1 = "..003688...1F";
+
+    private final List<Integer> findings;
+    private final byte[] where;
+
+    HeuristicFsFAT2(long fwVersion, byte[] where){
+        this.where = where;
+        String pattern = getPattern(fwVersion);
+        SimplyFind simplyfind = new SimplyFind(pattern, where);
+        this.findings = simplyfind.getResults();
+        /*
+        System.out.println("\t\tFAT32 # 2 +++++++++++++++++++++++++++++++");
+        for (Integer find : findings) {
+            System.out.println(getDetails(find));
+            System.out.println("------------------------------------------------------------------");
+        }
+        /*                                                                                                  FIXME
+        this.findings.removeIf(this::dropStep1);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep2);
+        if(findings.size() < 2)
+            return;
+
+        this.findings.removeIf(this::dropStep3);
+
+ */
+    }
+    private String getPattern(long fwVersion){
+        if (fwVersion < 15300) // & fwVersion >= 9300
+            return PATTERN0;
+        return PATTERN1;
+    }
+    // Let's focus on CBZ-ONLY statements
+    private boolean dropStep1(int offsetOfPatternFound){
+        return ((where[offsetOfPatternFound - 1] & (byte) 0b01111111) != 0x34);
+    }
+
+    private boolean dropStep2(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound - 4);
+
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation);
+        int afterJumpThirdExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        // Check first is 'MOV'; second is 'B'
+        return (! isMOV_REG(afterJumpSecondExpressions)) || ! isB(afterJumpThirdExpressions);
+    }
+
+    private boolean dropStep3(int offsetOfPatternFound){
+        int conditionalJumpLocation = getCBZConditionalJumpLocation(offsetOfPatternFound-4);
+        int afterJumpSecondExpressions = Converter.getLEint(where, conditionalJumpLocation+4);
+        int secondPairConditionalJumpLocation = ((afterJumpSecondExpressions & 0x3ffffff) * 4 + (conditionalJumpLocation+4)) & 0xfffff;
+
+        int thirdExpressionsPairElement1 = Converter.getLEint(where, secondPairConditionalJumpLocation);
+        int thirdExpressionsPairElement2 = Converter.getLEint(where, secondPairConditionalJumpLocation+4);
+        // Check first is 'ADD'; second is 'BL'
+        return (! isADD(thirdExpressionsPairElement1)) || (! isBL(thirdExpressionsPairElement2));
+    }
+
+    private int getCBZConditionalJumpLocation(int cbzOffsetInternal){
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        return ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+    }
+
+    @Override
+    public boolean isFound(){
+        return findings.size() == 1;
+    }
+
+    @Override
+    public boolean wantLessEntropy(){
+        return findings.size() > 1;
+    }
+
+    @Override
+    public int getOffset() throws Exception{
+        if(findings.isEmpty())
+            throw new Exception("Nothing found");
+        if (findings.size() > 1)
+            throw new Exception("Too many offsets");
+        return findings.get(0);
+    }
+
+    @Override
+    public boolean setOffsetsNearby(int offsetNearby) {
+        findings.removeIf(offset -> {
+            if (offset > offsetNearby)
+                return ! (offset < offsetNearby - 0xffff);
+            return ! (offset > offsetNearby - 0xffff);
+        });
+        return isFound();
+    }
+
+    public String getDetails(Integer value){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = value - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        return builder.toString();
+    }
+
+    @Override
+    public String getDetails(){
+        StringBuilder builder = new StringBuilder();
+        int cbzOffsetInternal = findings.get(0) - 4;
+        int cbzExpression = Converter.getLEint(where, cbzOffsetInternal);
+        int conditionalJumpLocation = ((cbzExpression >> 5 & 0x7FFFF) * 4 + cbzOffsetInternal) & 0xfffff;
+
+        int secondExpressionsPairElement1 = Converter.getLEint(where, conditionalJumpLocation);
+        int secondExpressionsPairElement2 = Converter.getLEint(where, conditionalJumpLocation+4);
+
+        builder.append(BinToAsmPrinter.printSimplified(cbzExpression, cbzOffsetInternal));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+4), cbzOffsetInternal+4));
+        builder.append(BinToAsmPrinter.printSimplified(Converter.getLEint(where, cbzOffsetInternal+8), cbzOffsetInternal+8));
+        builder.append("...\n");
+
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement1, conditionalJumpLocation));
+        builder.append(BinToAsmPrinter.printSimplified(secondExpressionsPairElement2, conditionalJumpLocation+4));
+
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsWizard.java b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsWizard.java
new file mode 100644
index 0000000..3d3d240
--- /dev/null
+++ b/src/main/java/nsusbloader/Utilities/patches/fs/finders/HeuristicFsWizard.java
@@ -0,0 +1,139 @@
+/*
+    Copyright 2018-2022 Dmitry Isaenko
+     
+    This file is part of NS-USBloader.
+
+    NS-USBloader is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    NS-USBloader is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with NS-USBloader.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nsusbloader.Utilities.patches.fs.finders;
+
+import nsusbloader.Utilities.patches.AHeuristic;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class HeuristicFsWizard {
+    /*
+    private final List<AHeuristic> all;
+    private final List<AHeuristic> found;
+    private final List<AHeuristic> wantLessEntropy;
+     */
+    private final boolean isFat;
+    private List<AHeuristic> all;
+    private List<AHeuristic> found;
+    private List<AHeuristic> wantLessEntropy;
+
+    private final StringBuilder errorsAndNotes;
+
+    private int offset1 = -1;
+    private int offset2 = -1;
+
+    public HeuristicFsWizard(long fwVersion, byte[] where, boolean isFat) throws Exception{
+        this.isFat = isFat;
+        this.errorsAndNotes = new StringBuilder();
+
+        if (isFat){
+            this.all = Arrays.asList(
+                    new HeuristicFsFAT1(fwVersion, where),
+                    new HeuristicFsFAT2(fwVersion, where)
+            );
+        }
+        else {
+            System.out.println("TODO: IMPLEMENT FOR EXFAT");
+            return;
+            /*
+            this.all = Arrays.asList(
+                    new HeuristicFsExFAT1(fwVersion, where),
+                    new HeuristicFsExFAT2(fwVersion, where)
+            );
+             */
+        }
+/*
+        this.found = all.stream()
+                .filter(AHeuristic::isFound)
+                .collect(Collectors.toList());
+
+        if (found.isEmpty())
+            throw new Exception("Nothing found!");
+
+        this.wantLessEntropy = all.stream()
+                .filter(AHeuristic::wantLessEntropy)
+                .collect(Collectors.toList());
+/*                                                                                                                      FIXME
+        shareOffsetsWithEachOther();
+
+        assignOffset1();
+        assignOffset2();
+         */
+    }
+
+    private void shareOffsetsWithEachOther(){
+        for (AHeuristic es : wantLessEntropy) {
+            if (shareWithNext(es))
+                return;
+        }
+    }
+    private boolean shareWithNext(AHeuristic es){
+        try {
+            for (AHeuristic foundEs : found) {
+                if (es.setOffsetsNearby(foundEs.getOffset())) {
+                    found.add(es);
+                    wantLessEntropy.remove(es);
+                    shareOffsetsWithEachOther();
+                    return true;
+                }
+            }
+        }
+        catch (Exception e){ e.printStackTrace(); }
+        return false;
+    }
+
+    private void assignOffset1(){
+        try {
+            offset1 = all.get(0).getOffset();
+        }
+        catch (Exception e){ errorsAndNotes.append(e.getLocalizedMessage()).append("\n"); }
+    }
+    private void assignOffset2(){
+        try {
+            offset2 = all.get(1).getOffset();
+        }
+        catch (Exception e){ errorsAndNotes.append(e.getLocalizedMessage()).append("\n"); }
+    }
+
+    public String getErrorsAndNotes(){
+        return errorsAndNotes.toString();
+    }
+
+    public String getDebug(){
+        StringBuilder builder = new StringBuilder();
+        if (isFat)
+            builder.append("\t\t--[  FAT32  ]--\n");
+        else
+            builder.append("\t\t--[  ExFAT  ]--\n");
+        if (all.get(0).isFound()){
+            builder.append("\t\t-=== 1 ===-\n");
+            builder.append(all.get(0).getDetails());
+        }
+        if (all.get(1).isFound()){
+            builder.append("\t\t-=== 2 ===-\n");
+            builder.append(all.get(1).getDetails());
+        }
+        return builder.toString();
+    }
+
+    public int getOffset1() { return offset1; }
+    public int getOffset2() { return offset2; }
+}
diff --git a/src/main/resources/PatchesTab.fxml b/src/main/resources/PatchesTab.fxml
index 801bf17..d888c2c 100644
--- a/src/main/resources/PatchesTab.fxml
+++ b/src/main/resources/PatchesTab.fxml
@@ -14,7 +14,7 @@
 <?import javafx.scene.shape.SVGPath?>
 <?import javafx.scene.text.Font?>
 
-<ScrollPane fitToWidth="true" onDragDropped="#handleDrop" onDragOver="#handleDragOver" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nsusbloader.Controllers.PatchesController">
+<ScrollPane fitToWidth="true" onDragDropped="#handleDrop" onDragOver="#handleDragOver" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nsusbloader.Controllers.PatchesController">
 <VBox fx:id="patchesToolPane" spacing="15.0">
    <Pane minHeight="-Infinity" prefHeight="10.0" style="-fx-background-color: linear-gradient(from 41px 34px to 50px 50px, reflect,  #2cd882 40%, transparent  45%);" />
    <HBox alignment="CENTER">
@@ -110,9 +110,10 @@
       </children>
    </HBox>
    <Pane VBox.vgrow="ALWAYS" />
-   <HBox alignment="CENTER">
+   <HBox alignment="CENTER" spacing="5.0">
       <children>
          <Button fx:id="makeEsBtn" contentDisplay="TOP" mnemonicParsing="false" styleClass="buttonUp" text="%tabPatches_Btn_MakeEs" />
+            <Button fx:id="makeFsBtn" contentDisplay="TOP" mnemonicParsing="false" styleClass="buttonUp" text="%tabPatches_Btn_MakeFs" />
       </children>
    </HBox>
    <padding>