Convert the NUSDataProvider class into an interfacer. All Utility functions are moved to a new DataProviderUtils class.

Changes the return values of the raw TMD/TICKET/CERT to Optionals.
Replacing some more null values with Optionals
This commit is contained in:
Maschell 2019-04-10 18:43:44 +02:00
parent 40cb09873d
commit d08a42719a
11 changed files with 287 additions and 306 deletions

View File

@ -17,17 +17,20 @@
package de.mas.wiiu.jnus; package de.mas.wiiu.jnus;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.implementations.NUSDataProvider; import de.mas.wiiu.jnus.implementations.NUSDataProvider;
import de.mas.wiiu.jnus.utils.DataProviderUtils;
import de.mas.wiiu.jnus.utils.FileUtils; import de.mas.wiiu.jnus.utils.FileUtils;
import de.mas.wiiu.jnus.utils.Parallelizable; import de.mas.wiiu.jnus.utils.Parallelizable;
import de.mas.wiiu.jnus.utils.Utils; import de.mas.wiiu.jnus.utils.Utils;
@ -68,7 +71,7 @@ public final class ExtractionService {
Utils.createDir(outputFolder); Utils.createDir(outputFolder);
NUSDataProvider dataProvider = getDataProvider(); NUSDataProvider dataProvider = getDataProvider();
for (Content c : list) { for (Content c : list) {
dataProvider.saveContentH3Hash(c, outputFolder); DataProviderUtils.saveContentH3Hash(dataProvider, c, outputFolder);
} }
} }
@ -87,9 +90,9 @@ public final class ExtractionService {
public void extractEncryptedContentTo(Content content, String outputFolder, boolean withHashes) throws IOException { public void extractEncryptedContentTo(Content content, String outputFolder, boolean withHashes) throws IOException {
NUSDataProvider dataProvider = getDataProvider(); NUSDataProvider dataProvider = getDataProvider();
if (withHashes) { if (withHashes) {
dataProvider.saveEncryptedContentWithH3Hash(content, outputFolder); DataProviderUtils.saveEncryptedContentWithH3Hash(dataProvider, content, outputFolder);
} else { } else {
dataProvider.saveEncryptedContent(content, outputFolder); DataProviderUtils.saveEncryptedContent(dataProvider, content, outputFolder);
} }
} }
@ -117,12 +120,7 @@ public final class ExtractionService {
public void extractTMDTo(String output) throws IOException { public void extractTMDTo(String output) throws IOException {
Utils.createDir(output); Utils.createDir(output);
byte[] rawTMD = getDataProvider().getRawTMD(); byte[] rawTMD = getDataProvider().getRawTMD().orElseThrow(() -> new FileNotFoundException("TMD not found"));
if (rawTMD == null || rawTMD.length == 0) {
log.info("Couldn't write TMD: No TMD loaded");
return;
}
String tmd_path = output + File.separator + Settings.TMD_FILENAME; String tmd_path = output + File.separator + Settings.TMD_FILENAME;
log.info("Extracting TMD to: " + tmd_path); log.info("Extracting TMD to: " + tmd_path);
FileUtils.saveByteArrayToFile(tmd_path, rawTMD); FileUtils.saveByteArrayToFile(tmd_path, rawTMD);
@ -131,29 +129,25 @@ public final class ExtractionService {
public boolean extractTicketTo(String output) throws IOException { public boolean extractTicketTo(String output) throws IOException {
Utils.createDir(output); Utils.createDir(output);
byte[] rawTicket = getDataProvider().getRawTicket(); byte[] rawTicket = getDataProvider().getRawTicket().orElseThrow(() -> new FileNotFoundException("Ticket not found"));
if (rawTicket == null || rawTicket.length == 0) {
log.info("Couldn't write Ticket: No Ticket loaded");
return false;
}
String ticket_path = output + File.separator + Settings.TICKET_FILENAME; String ticket_path = output + File.separator + Settings.TICKET_FILENAME;
log.info("Extracting Ticket to: " + ticket_path); log.info("Extracting Ticket to: " + ticket_path);
return FileUtils.saveByteArrayToFile(ticket_path, rawTicket); return FileUtils.saveByteArrayToFile(ticket_path, rawTicket);
} }
public void extractCertTo(String output) throws IOException { public boolean extractCertTo(String output) throws IOException {
Utils.createDir(output); Utils.createDir(output);
byte[] rawCert = getDataProvider().getRawCert(); Optional<byte[]> dataOpt = getDataProvider().getRawCert();
if (rawCert == null || rawCert.length == 0) { if (!dataOpt.isPresent()) {
log.info("Couldn't write Cert: No Cert loaded"); return false;
return;
} }
String cert_path = output + File.separator + Settings.CERT_FILENAME;
log.info("Extracting Cert to: " + cert_path); String path = output + File.separator + Settings.CERT_FILENAME;
FileUtils.saveByteArrayToFile(cert_path, rawCert); log.info("Extracting Cert to: " + path);
return FileUtils.saveByteArrayToFile(path, dataOpt.get());
} }
public void extractAll(String outputFolder) throws IOException { public void extractAll(String outputFolder) throws IOException {
@ -166,8 +160,5 @@ public final class ExtractionService {
} }
public byte[] getBytesFromContent(Content c, long offset, long size) throws IOException {
return getDataProvider().getChunkFromContent(c, offset, (int) size);
}
} }

View File

@ -40,13 +40,10 @@ abstract class NUSTitleLoader {
NUSDataProvider dataProvider = getDataProvider(result, config); NUSDataProvider dataProvider = getDataProvider(result, config);
result.setDataProvider(dataProvider); result.setDataProvider(dataProvider);
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD()); byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0));
result.setTMD(tmd);
if (tmd == null) { TMD tmd = TMD.parseTMD(tmdData);
log.info("TMD not found."); result.setTMD(tmd);
throw new Exception();
}
if (config.isNoDecryption()) { if (config.isNoDecryption()) {
return result; return result;
@ -54,17 +51,19 @@ abstract class NUSTitleLoader {
Ticket ticket = config.getTicket(); Ticket ticket = config.getTicket();
if (ticket == null) { if (ticket == null) {
ticket = Ticket.parseTicket(dataProvider.getRawTicket()); Optional<byte[]> ticketOpt = dataProvider.getRawTicket();
if (ticketOpt.isPresent()) {
ticket = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
}
}
if(ticket == null) {
new ParseException("Failed to get ticket data",0);
} }
result.setTicket(ticket); result.setTicket(ticket);
Content fstContent = tmd.getContentByIndex(0); Content fstContent = tmd.getContentByIndex(0);
InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0); InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0, Optional.of(fstContent.getEncryptedFileSize()));
if (fstContentEncryptedStream == null) {
log.warning("FST is null");
return null;
}
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize()); byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());

View File

@ -14,149 +14,21 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
****************************************************************************/ ****************************************************************************/
package de.mas.wiiu.jnus.implementations; package de.mas.wiiu.jnus.implementations;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import de.mas.wiiu.jnus.NUSTitle;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.utils.FileUtils;
import de.mas.wiiu.jnus.utils.HashUtil;
import de.mas.wiiu.jnus.utils.StreamUtils; import de.mas.wiiu.jnus.utils.StreamUtils;
import de.mas.wiiu.jnus.utils.Utils;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.java.Log;
@Log public interface NUSDataProvider {
/**
* Service Methods for loading NUS/Content data from different sources
*
* @author Maschell
*
*/
public abstract class NUSDataProvider {
@Getter private final NUSTitle NUSTitle;
public NUSDataProvider(NUSTitle title) { default public byte[] getChunkFromContent(Content content, long offset, int size) throws IOException {
this.NUSTitle = title; return StreamUtils.getBytesFromStream(getInputStreamFromContent(content, offset, Optional.of((long) size)), size);
}
/**
* Saves the given content encrypted with his .h3 file in the given directory. The Target directory will be created if it's missing. If the content is not
* hashed, no .h3 will be saved
*
* @param content
* Content that should be saved
* @param outputFolder
* Target directory where the files will be stored in.
* @throws IOException
*/
public void saveEncryptedContentWithH3Hash(@NonNull Content content, @NonNull String outputFolder) throws IOException {
saveContentH3Hash(content, outputFolder);
saveEncryptedContent(content, outputFolder);
}
/**
* Saves the .h3 file of the given content into the given directory. The Target directory will be created if it's missing. If the content is not hashed, no
* .h3 will be saved
*
* @param content
* The content of which the h3 hashes should be saved
* @param outputFolder
* @return
* @throws IOException
*/
public void saveContentH3Hash(@NonNull Content content, @NonNull String outputFolder) throws IOException {
if (!content.isHashed()) {
return;
}
String h3Filename = String.format("%08X%s", content.getID(), Settings.H3_EXTENTION);
File output = new File(outputFolder + File.separator + h3Filename);
if (output.exists()) {
if (Arrays.equals(content.getSHA2Hash(), HashUtil.hashSHA1(output))) {
log.info(h3Filename + " already exists");
return;
} else {
if (Arrays.equals(content.getSHA2Hash(), Arrays.copyOf(HashUtil.hashSHA256(output), 20))) { // 0005000c1f941200 used sha256 instead of SHA1
log.info(h3Filename + " already exists");
return;
}
log.warning(h3Filename + " already exists but hash is differrent than expected.");
}
}
byte[] hash = getContentH3Hash(content);
if (hash == null || hash.length == 0) {
return;
}
log.warning("Saving " + h3Filename + " ");
FileUtils.saveByteArrayToFile(output, hash);
}
/**
* Saves the given content encrypted in the given directory. The Target directory will be created if it's missing. If the content is not encrypted at all,
* it will be just saved anyway.
*
* @param content
* Content that should be saved
* @param outputFolder
* Target directory where the files will be stored in.
* @return
* @throws IOException
*/
public void saveEncryptedContent(@NonNull Content content, @NonNull String outputFolder) throws IOException {
int maxTries = 3;
int i = 0;
while (i < maxTries) {
File output = new File(outputFolder + File.separator + content.getFilename());
if (output.exists()) {
if (output.length() == content.getEncryptedFileSizeAligned()) {
log.info(content.getFilename() + "Encrypted content alreadys exists, skipped");
return;
} else {
log.warning(content.getFilename() + " Encrypted content alreadys exists, but the length is not as expected. Saving it again. "
+ output.length() + " " + content.getEncryptedFileSizeAligned() + " Difference: "
+ (output.length() - content.getEncryptedFileSizeAligned()));
}
}
Utils.createDir(outputFolder);
InputStream inputStream = getInputStreamFromContent(content, 0);
if (inputStream == null) {
log.info(content.getFilename() + " Couldn't save encrypted content. Input stream was null");
return;
}
log.warning("loading " + content.getFilename());
FileUtils.saveInputStreamToFile(output, inputStream, content.getEncryptedFileSizeAligned());
File outputNow = new File(outputFolder + File.separator + content.getFilename());
if (outputNow.exists()) {
if (outputNow.length() != content.getEncryptedFileSizeAligned()) {
log.warning(content.getFilename() + " Encrypted content length is not as expected. Saving it again. Loaded: " + outputNow.length()
+ " Expected: " + content.getEncryptedFileSizeAligned() + " Difference: "
+ (outputNow.length() - content.getEncryptedFileSizeAligned()));
i++;
continue;
} else {
break;
}
}
}
}
public byte[] getChunkFromContent(Content content, long offset, int size) throws IOException {
return StreamUtils.getBytesFromStream(getInputStreamFromContent(content, offset, Optional.of((long)size)), size);
} }
/** /**
@ -168,18 +40,17 @@ public abstract class NUSDataProvider {
*/ */
public abstract InputStream getInputStreamFromContent(Content content, long offset, Optional<Long> size) throws IOException; public abstract InputStream getInputStreamFromContent(Content content, long offset, Optional<Long> size) throws IOException;
public InputStream getInputStreamFromContent(Content content, long offset) throws IOException { default public InputStream getInputStreamFromContent(Content content, long offset) throws IOException {
return getInputStreamFromContent(content, offset, Optional.empty()); return getInputStreamFromContent(content, offset, Optional.empty());
} }
// TODO: JavaDocs public abstract Optional<byte[]> getContentH3Hash(Content content) throws IOException;
public abstract byte[] getContentH3Hash(Content content) throws IOException;
public abstract byte[] getRawTMD() throws IOException; public abstract Optional<byte[]> getRawTMD() throws IOException;
public abstract byte[] getRawTicket() throws IOException; public abstract Optional<byte[]> getRawTicket() throws IOException;
public abstract byte[] getRawCert() throws IOException; public abstract Optional<byte[]> getRawCert() throws IOException;
public abstract void cleanup() throws IOException; public abstract void cleanup() throws IOException;

View File

@ -33,11 +33,10 @@ import lombok.Getter;
import lombok.extern.java.Log; import lombok.extern.java.Log;
@Log @Log
public final class NUSDataProviderLocal extends NUSDataProvider { public final class NUSDataProviderLocal implements NUSDataProvider {
@Getter private final String localPath; @Getter private final String localPath;
public NUSDataProviderLocal(NUSTitle nustitle, String localPath) { public NUSDataProviderLocal(String localPath) {
super(nustitle);
this.localPath = localPath; this.localPath = localPath;
} }
@ -59,48 +58,48 @@ public final class NUSDataProviderLocal extends NUSDataProvider {
} }
@Override @Override
public byte[] getContentH3Hash(Content content) throws IOException { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
String h3Filename = String.format("%08X%s", content.getID(), Settings.H3_EXTENTION); String h3Filename = String.format("%08X%s", content.getID(), Settings.H3_EXTENTION);
File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), h3Filename); File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), h3Filename);
if (filepath == null || !filepath.exists()) { if (filepath == null || !filepath.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + h3Filename + "\", file does not exist"; String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + h3Filename + "\", file does not exist";
log.warning(errormsg); log.warning(errormsg);
return new byte[0]; throw new FileNotFoundException(errormsg);
} }
return Files.readAllBytes(filepath.toPath()); return Optional.of(Files.readAllBytes(filepath.toPath()));
} }
@Override @Override
public byte[] getRawTMD() throws IOException { public Optional<byte[]> getRawTMD() throws IOException {
File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.TMD_FILENAME); File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.TMD_FILENAME);
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.TMD_FILENAME + "\", file does not exist"; String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.TMD_FILENAME + "\", file does not exist";
log.warning(errormsg); log.warning(errormsg);
throw new FileNotFoundException(errormsg); throw new FileNotFoundException(errormsg);
} }
return Files.readAllBytes(file.toPath()); return Optional.of(Files.readAllBytes(file.toPath()));
} }
@Override @Override
public byte[] getRawTicket() throws IOException { public Optional<byte[]> getRawTicket() throws IOException {
File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.TICKET_FILENAME); File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.TICKET_FILENAME);
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.TICKET_FILENAME + "\", file does not exist"; String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.TICKET_FILENAME + "\", file does not exist";
log.warning(errormsg); log.warning(errormsg);
throw new FileNotFoundException(errormsg); throw new FileNotFoundException(errormsg);
} }
return Files.readAllBytes(file.toPath()); return Optional.of(Files.readAllBytes(file.toPath()));
} }
@Override @Override
public byte[] getRawCert() throws IOException { public Optional<byte[]> getRawCert() throws IOException {
File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.CERT_FILENAME); File file = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), Settings.CERT_FILENAME);
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.CERT_FILENAME + "\", file does not exist"; String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + Settings.CERT_FILENAME + "\", file does not exist";
log.warning(errormsg); log.warning(errormsg);
throw new FileNotFoundException(errormsg); throw new FileNotFoundException(errormsg);
} }
return Files.readAllBytes(file.toPath()); return Optional.of(Files.readAllBytes(file.toPath()));
} }
@Override @Override

View File

@ -2,28 +2,26 @@ package de.mas.wiiu.jnus.implementations;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Optional; import java.util.Optional;
import de.mas.wiiu.jnus.NUSTitle;
import de.mas.wiiu.jnus.Settings; import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
import de.mas.wiiu.jnus.utils.StreamUtils; import de.mas.wiiu.jnus.utils.StreamUtils;
import lombok.Getter; import lombok.Getter;
public class NUSDataProviderLocalBackup extends NUSDataProvider { public class NUSDataProviderLocalBackup implements NUSDataProvider {
@Getter private final String localPath; @Getter private final String localPath;
private final short titleVersion; private final short titleVersion;
public NUSDataProviderLocalBackup(NUSTitle nustitle, String localPath) { public NUSDataProviderLocalBackup(String localPath) {
this(nustitle, localPath, (short) Settings.LATEST_TMD_VERSION); this(localPath, (short) Settings.LATEST_TMD_VERSION);
} }
public NUSDataProviderLocalBackup(NUSTitle nustitle, String localPath, short version) { public NUSDataProviderLocalBackup(String localPath, short version) {
super(nustitle);
this.localPath = localPath; this.localPath = localPath;
this.titleVersion = version; this.titleVersion = version;
} }
@ -36,7 +34,7 @@ public class NUSDataProviderLocalBackup extends NUSDataProvider {
public InputStream getInputStreamFromContent(Content content, long offset, Optional<Long> size) throws IOException { public InputStream getInputStreamFromContent(Content content, long offset, Optional<Long> size) throws IOException {
File filepath = new File(getFilePathOnDisk(content)); File filepath = new File(getFilePathOnDisk(content));
if (!filepath.exists()) { if (!filepath.exists()) {
return null; throw new FileNotFoundException(filepath.getAbsolutePath() + " was not found.");
} }
InputStream in = new FileInputStream(filepath); InputStream in = new FileInputStream(filepath);
StreamUtils.skipExactly(in, offset); StreamUtils.skipExactly(in, offset);
@ -44,40 +42,38 @@ public class NUSDataProviderLocalBackup extends NUSDataProvider {
} }
@Override @Override
public byte[] getContentH3Hash(Content content) throws IOException { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID()); String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
File h3File = new File(h3Path); File h3File = new File(h3Path);
if (!h3File.exists()) {
return new byte[0]; return Optional.of(Files.readAllBytes(h3File.toPath()));
}
return Files.readAllBytes(h3File.toPath());
} }
@Override @Override
public byte[] getRawTMD() throws IOException { public Optional<byte[]> getRawTMD() throws IOException {
String inputPath = getLocalPath(); String inputPath = getLocalPath();
String tmdPath = inputPath + File.separator + Settings.TMD_FILENAME; String tmdPath = inputPath + File.separator + Settings.TMD_FILENAME;
if (titleVersion != Settings.LATEST_TMD_VERSION) { if (titleVersion != Settings.LATEST_TMD_VERSION) {
tmdPath = inputPath + File.separator + "v" + titleVersion + File.separator + Settings.TMD_FILENAME; tmdPath = inputPath + File.separator + "v" + titleVersion + File.separator + Settings.TMD_FILENAME;
} }
File tmdFile = new File(tmdPath); File tmdFile = new File(tmdPath);
return Files.readAllBytes(tmdFile.toPath()); return Optional.of(Files.readAllBytes(tmdFile.toPath()));
} }
@Override @Override
public byte[] getRawTicket() throws IOException { public Optional<byte[]> getRawTicket() throws IOException {
String inputPath = getLocalPath(); String inputPath = getLocalPath();
String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME; String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME;
File ticketFile = new File(ticketPath); File ticketFile = new File(ticketPath);
return Files.readAllBytes(ticketFile.toPath()); return Optional.of(Files.readAllBytes(ticketFile.toPath()));
} }
@Override @Override
public byte[] getRawCert() throws IOException { public Optional<byte[]> getRawCert() throws IOException {
String inputPath = getLocalPath(); String inputPath = getLocalPath();
String certPath = inputPath + File.separator + Settings.CERT_FILENAME; String certPath = inputPath + File.separator + Settings.CERT_FILENAME;
File certFile = new File(certPath); File certFile = new File(certPath);
return Files.readAllBytes(certFile.toPath()); return Optional.of(Files.readAllBytes(certFile.toPath()));
} }
@Override @Override

View File

@ -16,25 +16,21 @@
****************************************************************************/ ****************************************************************************/
package de.mas.wiiu.jnus.implementations; package de.mas.wiiu.jnus.implementations;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional; import java.util.Optional;
import de.mas.wiiu.jnus.NUSTitle;
import de.mas.wiiu.jnus.Settings; import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.utils.Parallelizable; import de.mas.wiiu.jnus.utils.Parallelizable;
import de.mas.wiiu.jnus.utils.download.NUSDownloadService; import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
import lombok.Getter; import lombok.Getter;
public class NUSDataProviderRemote extends NUSDataProvider implements Parallelizable { public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
@Getter private final int version; @Getter private final int version;
@Getter private final long titleID; @Getter private final long titleID;
public NUSDataProviderRemote(NUSTitle title, int version, long titleID) { public NUSDataProviderRemote(int version, long titleID) {
super(title);
this.version = version; this.version = version;
this.titleID = titleID; this.titleID = titleID;
} }
@ -46,53 +42,49 @@ public class NUSDataProviderRemote extends NUSDataProvider implements Paralleliz
} }
private String getRemoteURL(Content content) { private String getRemoteURL(Content content) {
return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(), content.getID()); return String.format("%016x/%08X", titleID, content.getID());
} }
@Override @Override
public byte[] getContentH3Hash(Content content) throws IOException { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
String url = getRemoteURL(content) + Settings.H3_EXTENTION; String url = getRemoteURL(content) + Settings.H3_EXTENTION;
return downloadService.downloadToByteArray(url);
byte[] res = downloadService.downloadToByteArray(url);
if (res == null || res.length == 0) {
return Optional.empty();
}
return Optional.of(res);
} }
@Override @Override
public byte[] getRawTMD() throws IOException { public Optional<byte[]> getRawTMD() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getTitleID(); long titleID = getTitleID();
int version = getVersion(); int version = getVersion();
return downloadService.downloadTMDToByteArray(titleID, version); byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
}
@Override if (res == null || res.length == 0) {
public byte[] getRawTicket() throws IOException { return Optional.empty();
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getNUSTitle().getTMD().getTitleID();
return downloadService.downloadTicketToByteArray(titleID);
}
@Override
public byte[] getRawCert() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
byte[] defaultCert = downloadService.downloadDefaultCertToByteArray();
TMD tmd = getNUSTitle().getTMD();
byte[] result = new byte[0];
try {
ByteArrayOutputStream fos = new ByteArrayOutputStream();
fos.write(tmd.getCert1());
fos.write(tmd.getCert2());
fos.write(defaultCert);
result = fos.toByteArray();
fos.close();
} catch (Exception e) {
e.printStackTrace();
} }
return result; return Optional.of(res);
}
@Override
public Optional<byte[]> getRawTicket() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
byte[] res = downloadService.downloadTicketToByteArray(titleID);
if (res == null || res.length == 0) {
return Optional.empty();
}
return Optional.of(res);
}
@Override
public Optional<byte[]> getRawCert() throws IOException {
return Optional.empty();
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/**************************************************************************** /****************************************************************************
* Copyright (C) 2016-2018 Maschell * Copyright (C) 2016-2019 Maschell
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,9 +20,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional; import java.util.Optional;
import de.mas.wiiu.jnus.NUSTitle;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition; import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDPartitionHeader; import de.mas.wiiu.jnus.implementations.wud.parser.WUDPartitionHeader;
@ -31,29 +28,20 @@ import lombok.Getter;
import lombok.extern.java.Log; import lombok.extern.java.Log;
@Log @Log
public class NUSDataProviderWUD extends NUSDataProvider { public class NUSDataProviderWUD implements NUSDataProvider {
@Getter private final WUDGamePartition gamePartition; @Getter private final WUDGamePartition gamePartition;
@Getter private final WUDDiscReader discReader; @Getter private final WUDDiscReader discReader;
private final TMD tmd; public NUSDataProviderWUD(WUDGamePartition gamePartition, WUDDiscReader discReader) {
public NUSDataProviderWUD(NUSTitle title, WUDGamePartition gamePartition, WUDDiscReader discReader) {
super(title);
this.gamePartition = gamePartition; this.gamePartition = gamePartition;
this.discReader = discReader; this.discReader = discReader;
this.tmd = TMD.parseTMD(getRawTMD());
} }
public long getOffsetInWUD(Content content) { public long getOffsetInWUD(Content content) {
if (content.getContentFSTInfo() == null) { if (content.getIndex() == 0) { // Index 0 is the FST which is at the beginning of the partion;
return getAbsoluteReadOffset(); return getGamePartition().getAbsolutePartitionOffset();
} else {
return getAbsoluteReadOffset() + content.getContentFSTInfo().getOffset();
} }
} return getGamePartition().getAbsolutePartitionOffset() + content.getContentFSTInfo().getOffset();
public long getAbsoluteReadOffset() {
return (long) Settings.WIIU_DECRYPTED_AREA_OFFSET + getGamePartition().getPartitionOffset();
} }
@Override @Override
@ -66,45 +54,34 @@ public class NUSDataProviderWUD extends NUSDataProvider {
WUDDiscReader discReader = getDiscReader(); WUDDiscReader discReader = getDiscReader();
long offset = getOffsetInWUD(content) + fileOffsetBlock; long offset = getOffsetInWUD(content) + fileOffsetBlock;
long usedSize = content.getEncryptedFileSize() - fileOffsetBlock; long usedSize = content.getEncryptedFileSize() - fileOffsetBlock;
if(size.isPresent()) { if (size.isPresent()) {
usedSize = size.get(); usedSize = size.get();
} }
return discReader.readEncryptedToInputStream(offset, usedSize); return discReader.readEncryptedToInputStream(offset, usedSize);
} }
@Override @Override
public byte[] getContentH3Hash(Content content) throws IOException { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
if (getGamePartitionHeader() == null) {
log.warning("GamePartitionHeader is null");
return null;
}
if (!getGamePartitionHeader().isCalculatedHashes()) { if (!getGamePartitionHeader().isCalculatedHashes()) {
log.info("Calculating h3 hashes"); log.info("Calculating h3 hashes");
getGamePartitionHeader().calculateHashes(getTMD().getAllContents()); getGamePartitionHeader().calculateHashes(getGamePartition().getTmd().getAllContents());
} }
return getGamePartitionHeader().getH3Hash(content); return getGamePartitionHeader().getH3Hash(content);
} }
public TMD getTMD() { @Override
return tmd; public Optional<byte[]> getRawTMD() {
return Optional.of(getGamePartition().getRawTMD());
} }
@Override @Override
public byte[] getRawTMD() { public Optional<byte[]> getRawTicket() {
return getGamePartition().getRawTMD(); return Optional.of(getGamePartition().getRawTicket());
} }
@Override @Override
public byte[] getRawTicket() { public Optional<byte[]> getRawCert() throws IOException {
return getGamePartition().getRawTicket(); return Optional.of(getGamePartition().getRawCert());
}
@Override
public byte[] getRawCert() throws IOException {
return getGamePartition().getRawCert();
} }
public WUDPartitionHeader getGamePartitionHeader() { public WUDPartitionHeader getGamePartitionHeader() {

View File

@ -35,12 +35,11 @@ import lombok.Setter;
import lombok.extern.java.Log; import lombok.extern.java.Log;
@Log @Log
public class NUSDataProviderWoomy extends NUSDataProvider { public class NUSDataProviderWoomy implements NUSDataProvider {
@Getter private final WoomyInfo woomyInfo; @Getter private final WoomyInfo woomyInfo;
@Setter(AccessLevel.PRIVATE) private WoomyZipFile woomyZipFile; @Setter(AccessLevel.PRIVATE) private WoomyZipFile woomyZipFile;
public NUSDataProviderWoomy(NUSTitle title, WoomyInfo woomyInfo) { public NUSDataProviderWoomy(WoomyInfo woomyInfo) {
super(title);
this.woomyInfo = woomyInfo; this.woomyInfo = woomyInfo;
} }
@ -56,19 +55,19 @@ public class NUSDataProviderWoomy extends NUSDataProvider {
} }
@Override @Override
public byte[] getContentH3Hash(Content content) throws IOException { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase()); ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
if (entry != null) { if (entry != null) {
WoomyZipFile zipFile = getNewWoomyZipFile(); WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry); byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close(); zipFile.close();
return result; return Optional.of(result);
} }
return new byte[0]; return Optional.empty();
} }
@Override @Override
public byte[] getRawTMD() throws IOException { public Optional<byte[]> getRawTMD() throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME); ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME);
if (entry == null) { if (entry == null) {
log.warning(Settings.TMD_FILENAME + " not found in woomy file"); log.warning(Settings.TMD_FILENAME + " not found in woomy file");
@ -77,11 +76,11 @@ public class NUSDataProviderWoomy extends NUSDataProvider {
WoomyZipFile zipFile = getNewWoomyZipFile(); WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry); byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close(); zipFile.close();
return result; return Optional.of(result);
} }
@Override @Override
public byte[] getRawTicket() throws IOException { public Optional<byte[]> getRawTicket() throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME); ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME);
if (entry == null) { if (entry == null) {
log.warning(Settings.TICKET_FILENAME + " not found in woomy file"); log.warning(Settings.TICKET_FILENAME + " not found in woomy file");
@ -91,7 +90,7 @@ public class NUSDataProviderWoomy extends NUSDataProvider {
WoomyZipFile zipFile = getNewWoomyZipFile(); WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry); byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close(); zipFile.close();
return result; return Optional.of(result);
} }
public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException { public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException {
@ -113,7 +112,7 @@ public class NUSDataProviderWoomy extends NUSDataProvider {
} }
@Override @Override
public byte[] getRawCert() throws IOException { public Optional<byte[]> getRawCert() throws IOException {
return new byte[0]; return Optional.empty();
} }
} }

View File

@ -1,5 +1,5 @@
/**************************************************************************** /****************************************************************************
* Copyright (C) 2016-2018 Maschell * Copyright (C) 2016-2019 Maschell
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,19 +16,24 @@
****************************************************************************/ ****************************************************************************/
package de.mas.wiiu.jnus.implementations.wud.parser; package de.mas.wiiu.jnus.implementations.wud.parser;
import java.text.ParseException;
import de.mas.wiiu.jnus.entities.TMD;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WUDGamePartition extends WUDPartition { public class WUDGamePartition extends WUDPartition {
private final TMD tmd;
private final byte[] rawTMD; private final byte[] rawTMD;
private final byte[] rawCert; private final byte[] rawCert;
private final byte[] rawTicket; private final byte[] rawTicket;
public WUDGamePartition(String partitionName, long partitionOffset, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) { public WUDGamePartition(String partitionName, long partitionOffset, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) throws ParseException {
super(partitionName, partitionOffset); super(partitionName, partitionOffset);
this.rawTMD = rawTMD; this.rawTMD = rawTMD;
this.tmd = TMD.parseTMD(rawTMD);
this.rawCert = rawCert; this.rawCert = rawCert;
this.rawTicket = rawTicket; this.rawTicket = rawTicket;
} }

View File

@ -1,5 +1,5 @@
/**************************************************************************** /****************************************************************************
* Copyright (C) 2016-2018 Maschell * Copyright (C) 2016-2019 Maschell
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -19,10 +19,10 @@ package de.mas.wiiu.jnus.implementations.wud.parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.utils.ByteUtils; import de.mas.wiiu.jnus.utils.ByteUtils;
@ -52,13 +52,13 @@ public final class WUDPartitionHeader {
getH3Hashes().put(index, hash); getH3Hashes().put(index, hash);
} }
public byte[] getH3Hash(Content content) { public Optional<byte[]> getH3Hash(Content content) {
if (content == null) { if (content == null) {
log.info("Can't find h3 hash, given content is null."); log.info("Can't find h3 hash, given content is null.");
return null; return Optional.empty();
} }
return getH3Hashes().get(content.getIndex()); return Optional.of(getH3Hashes().get(content.getIndex()));
} }
public void calculateHashes(Map<Integer, Content> allContents) { public void calculateHashes(Map<Integer, Content> allContents) {

View File

@ -0,0 +1,152 @@
/****************************************************************************
* Copyright (C) 2016-2018 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 <http://www.gnu.org/licenses/>.
****************************************************************************/
package de.mas.wiiu.jnus.utils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
import lombok.NonNull;
import lombok.extern.java.Log;
@Log
/**
* Service Methods for loading NUS/Content data from different sources
*
* @author Maschell
*
*/
public class DataProviderUtils {
private DataProviderUtils() {
}
/**
* Saves the given content encrypted with his .h3 file in the given directory. The Target directory will be created if it's missing. If the content is not
* hashed, no .h3 will be saved
*
* @param content
* Content that should be saved
* @param outputFolder
* Target directory where the files will be stored in.
* @throws IOException
*/
public static void saveEncryptedContentWithH3Hash(@NonNull NUSDataProvider dataProvider, @NonNull Content content, @NonNull String outputFolder)
throws IOException {
saveContentH3Hash(dataProvider, content, outputFolder);
saveEncryptedContent(dataProvider, content, outputFolder);
}
/**
* Saves the .h3 file of the given content into the given directory. The Target directory will be created if it's missing. If the content is not hashed, no
* .h3 will be saved
*
* @param content
* The content of which the h3 hashes should be saved
* @param outputFolder
* @return
* @throws IOException
*/
public static boolean saveContentH3Hash(@NonNull NUSDataProvider dataProvider, @NonNull Content content, @NonNull String outputFolder) throws IOException {
if (!content.isHashed()) {
return false;
}
String h3Filename = String.format("%08X%s", content.getID(), Settings.H3_EXTENTION);
File output = new File(outputFolder + File.separator + h3Filename);
if (output.exists()) {
if (Arrays.equals(content.getSHA2Hash(), HashUtil.hashSHA1(output))) {
log.info(h3Filename + " already exists");
return false;
} else {
if (Arrays.equals(content.getSHA2Hash(), Arrays.copyOf(HashUtil.hashSHA256(output), 20))) { // 0005000c1f941200 used sha256 instead of SHA1
log.info(h3Filename + " already exists");
return false;
}
log.warning(h3Filename + " already exists but hash is differrent than expected.");
}
}
Optional<byte[]> hashOpt = dataProvider.getContentH3Hash(content);
if (!hashOpt.isPresent()) {
return false;
}
byte[] hash = hashOpt.get();
log.warning("Saving " + h3Filename + " ");
return FileUtils.saveByteArrayToFile(output, hash);
}
/**
* Saves the given content encrypted in the given directory. The Target directory will be created if it's missing. If the content is not encrypted at all,
* it will be just saved anyway.
*
* @param content
* Content that should be saved
* @param outputFolder
* Target directory where the files will be stored in.
* @return
* @throws IOException
*/
public static void saveEncryptedContent(@NonNull NUSDataProvider dataProvider, @NonNull Content content, @NonNull String outputFolder) throws IOException {
int maxTries = 3;
int i = 0;
while (i < maxTries) {
File output = new File(outputFolder + File.separator + content.getFilename());
if (output.exists()) {
if (output.length() == content.getEncryptedFileSizeAligned()) {
log.info(content.getFilename() + "Encrypted content alreadys exists, skipped");
return;
} else {
log.warning(content.getFilename() + " Encrypted content alreadys exists, but the length is not as expected. Saving it again. "
+ output.length() + " " + content.getEncryptedFileSizeAligned() + " Difference: "
+ (output.length() - content.getEncryptedFileSizeAligned()));
}
}
Utils.createDir(outputFolder);
InputStream inputStream = dataProvider.getInputStreamFromContent(content, 0);
if (inputStream == null) {
log.info(content.getFilename() + " Couldn't save encrypted content. Input stream was null");
return;
}
log.warning("loading " + content.getFilename());
FileUtils.saveInputStreamToFile(output, inputStream, content.getEncryptedFileSizeAligned());
File outputNow = new File(outputFolder + File.separator + content.getFilename());
if (outputNow.exists()) {
if (outputNow.length() != content.getEncryptedFileSizeAligned()) {
log.warning(content.getFilename() + " Encrypted content length is not as expected. Saving it again. Loaded: " + outputNow.length()
+ " Expected: " + content.getEncryptedFileSizeAligned() + " Difference: "
+ (outputNow.length() - content.getEncryptedFileSizeAligned()));
i++;
continue;
} else {
break;
}
}
}
}
}