diff --git a/src/de/mas/wiiu/jnus/DecryptionService.java b/src/de/mas/wiiu/jnus/DecryptionService.java
new file mode 100644
index 0000000..46144c9
--- /dev/null
+++ b/src/de/mas/wiiu/jnus/DecryptionService.java
@@ -0,0 +1,200 @@
+/****************************************************************************
+ * Copyright (C) 2016-2019 Maschell
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ ****************************************************************************/
+package de.mas.wiiu.jnus;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+
+import de.mas.wiiu.jnus.entities.content.Content;
+import de.mas.wiiu.jnus.entities.fst.FSTEntry;
+import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
+import de.mas.wiiu.jnus.utils.CheckSumWrongException;
+import de.mas.wiiu.jnus.utils.FSTUtils;
+import de.mas.wiiu.jnus.utils.FileUtils;
+import de.mas.wiiu.jnus.utils.HashUtil;
+import de.mas.wiiu.jnus.utils.Utils;
+import lombok.val;
+import lombok.extern.java.Log;
+
+@Log
+public final class DecryptionService {
+
+ private final FSTDataProvider dataProvider;
+
+ private boolean parallelizable = false;
+
+ public static DecryptionService getInstance(FSTDataProvider dataProvider) {
+ return new DecryptionService(dataProvider);
+ }
+
+ private DecryptionService(FSTDataProvider dataProvider) {
+ // if (dataProvider instanceof Parallelizable) {
+ // parallelizable = true;
+ // }
+ this.dataProvider = dataProvider;
+ }
+
+ public void decryptFSTEntryTo(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) {
+ try {
+ decryptFSTEntryToAsync(useFullPath, entry, outputPath, skipExistingFile).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public CompletableFuture decryptFSTEntryToAsync(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ if (entry.isNotInPackage() || entry.getContent() == null) {
+ return;
+ }
+
+ log.info("Decrypting " + entry.getFilename());
+
+ String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getFilename()).toString();
+ String fullPath = new StringBuilder().append(outputPath).toString();
+
+ if (useFullPath) {
+ targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
+ fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
+ if (entry.isDir()) { // If the entry is a directory. Create it and return.
+ Utils.createDir(targetFilePath);
+ return;
+ }
+ } else if (entry.isDir()) {
+ return;
+ }
+
+ if (!Utils.createDir(fullPath)) {
+ return;
+ }
+
+ File target = new File(targetFilePath);
+
+ if (skipExistingFile) {
+ File targetFile = new File(targetFilePath);
+ if (targetFile.exists()) {
+ if (entry.isDir()) {
+ return;
+ }
+ if (targetFile.length() == entry.getFileSize()) {
+ Content c = entry.getContent();
+ if (c.isHashed()) {
+ log.info("File already exists: " + entry.getFilename());
+ return;
+ } else {
+ if (Arrays.equals(HashUtil.hashSHA1(target, (int) c.getDecryptedFileSize()), c.getSHA2Hash())) {
+ log.info("File already exists: " + entry.getFilename());
+ return;
+ } else {
+ log.info("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename());
+ }
+ }
+
+ } else {
+ log.info("File already exists but the filesize doesn't match: " + entry.getFilename());
+ }
+ }
+ }
+
+ // to avoid having fragmented files.
+ FileUtils.FileAsOutputStreamWrapper(new File(targetFilePath), entry.getFileSize(),
+ newOutputStream -> decryptFSTEntryToStream(entry, newOutputStream));
+ } catch (Exception ex) {
+ throw new CompletionException(ex);
+ }
+ });
+ }
+
+ public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException {
+ dataProvider.readFileToStream(outputStream, entry, 0);
+ }
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // Decrypt FSTEntry to OutputStream
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptFSTEntryTo(String entryFullPath, OutputStream outputStream) throws IOException, CheckSumWrongException {
+ FSTEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
+ .orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
+
+ decryptFSTEntryToStream(entry, outputStream);
+ }
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // Decrypt single FSTEntry to File
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles)
+ throws IOException, CheckSumWrongException {
+
+ FSTEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
+ .orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
+
+ decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
+ }
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // Decrypt list of FSTEntry to Files
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptAllFSTEntriesTo(String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
+ Utils.createDir(outputFolder + File.separator + "code");
+ Utils.createDir(outputFolder + File.separator + "content");
+ Utils.createDir(outputFolder + File.separator + "meta");
+ decryptFSTEntriesTo(true, ".*", outputFolder, skipExisting);
+ }
+
+ public void decryptFSTEntriesTo(String regEx, String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
+ decryptFSTEntriesTo(true, regEx, outputFolder, skipExisting);
+ }
+
+ public void decryptFSTEntriesTo(boolean fullPath, String regEx, String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
+ decryptFSTEntryListTo(fullPath, FSTUtils.getFSTEntriesByRegEx(dataProvider.getRoot(), regEx), outputFolder, skipExisting);
+ }
+
+ public void decryptFSTEntryListTo(List list, String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
+ decryptFSTEntryListTo(true, list, outputFolder, skipExisting);
+ }
+
+ public void decryptFSTEntryListTo(boolean fullPath, List list, String outputFolder, boolean skipExisting)
+ throws IOException, CheckSumWrongException {
+ if (parallelizable && Settings.ALLOW_PARALLELISATION) {
+ try {
+ decryptFSTEntryListToAsync(fullPath, list, outputFolder, skipExisting).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ for (val entry : list) {
+ decryptFSTEntryTo(fullPath, entry, outputFolder, skipExisting);
+ }
+ }
+ }
+
+ public CompletableFuture decryptFSTEntryListToAsync(boolean fullPath, List list, String outputFolder, boolean skipExisting)
+ throws IOException, CheckSumWrongException {
+ return CompletableFuture
+ .allOf(list.stream().map(entry -> decryptFSTEntryToAsync(fullPath, entry, outputFolder, skipExisting)).toArray(CompletableFuture[]::new));
+ }
+
+}