mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-17 13:39:19 +01:00
wip
This commit is contained in:
parent
15a1df1358
commit
e4d32e9910
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@
|
|||||||
/target/
|
/target/
|
||||||
.settings/
|
.settings/
|
||||||
.classpath
|
.classpath
|
||||||
|
test.1
|
||||||
|
test.2
|
||||||
|
@ -25,7 +25,8 @@ 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.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||||
@ -49,7 +50,7 @@ public final class DecryptionService {
|
|||||||
this.dataProvider = dataProvider;
|
this.dataProvider = dataProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decryptFSTEntryTo(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) {
|
public void decryptFSTEntryTo(boolean useFullPath, NodeEntry entry, String outputPath, boolean skipExistingFile) {
|
||||||
try {
|
try {
|
||||||
decryptFSTEntryToAsync(useFullPath, entry, outputPath, skipExistingFile).get();
|
decryptFSTEntryToAsync(useFullPath, entry, outputPath, skipExistingFile).get();
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
@ -57,26 +58,26 @@ public final class DecryptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> decryptFSTEntryToAsync(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) {
|
public CompletableFuture<Void> decryptFSTEntryToAsync(boolean useFullPath, NodeEntry entry, String outputPath, boolean skipExistingFile) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
return CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
if (entry.isNotInPackage()) {
|
if (entry.isLink()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Decrypting " + entry.getFilename());
|
log.info("Decrypting " + entry.getName());
|
||||||
|
|
||||||
String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getFilename()).toString();
|
String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getName()).toString();
|
||||||
String fullPath = new StringBuilder().append(outputPath).toString();
|
String fullPath = new StringBuilder().append(outputPath).toString();
|
||||||
|
|
||||||
if (useFullPath) {
|
if (useFullPath) {
|
||||||
targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
|
|
||||||
fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
|
fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
|
||||||
if (entry.isDir()) { // If the entry is a directory. Create it and return.
|
targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
|
||||||
|
if (entry.isDirectory()) { // If the entry is a directory. Create it and return.
|
||||||
Utils.createDir(targetFilePath);
|
Utils.createDir(targetFilePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (entry.isDir()) {
|
} else if (entry.isDirectory()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,16 +88,16 @@ public final class DecryptionService {
|
|||||||
if (skipExistingFile) {
|
if (skipExistingFile) {
|
||||||
File targetFile = new File(targetFilePath);
|
File targetFile = new File(targetFilePath);
|
||||||
if (targetFile.exists()) {
|
if (targetFile.exists()) {
|
||||||
if (entry.isDir()) {
|
if (entry.isDirectory()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (targetFile.length() == entry.getFileSize()) {
|
if (targetFile.length() == ((FileEntry) entry).getSize()) {
|
||||||
|
|
||||||
log.info("File already exists: " + entry.getFilename());
|
log.info("File already exists: " + entry.getName());
|
||||||
return;
|
return;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.info("File already exists but the filesize doesn't match: " + entry.getFilename());
|
log.info("File already exists but the filesize doesn't match: " + entry.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,26 +105,32 @@ public final class DecryptionService {
|
|||||||
File target = new File(targetFilePath);
|
File target = new File(targetFilePath);
|
||||||
|
|
||||||
// to avoid having fragmented files.
|
// to avoid having fragmented files.
|
||||||
FileUtils.FileAsOutputStreamWrapper(target, entry.getFileSize(), newOutputStream -> decryptFSTEntryToStream(entry, newOutputStream));
|
FileUtils.FileAsOutputStreamWrapper(target, ((FileEntry) entry).getSize(),
|
||||||
|
newOutputStream -> decryptFSTEntryToStream((FileEntry) entry, newOutputStream));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new CompletionException(ex);
|
throw new CompletionException(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException {
|
public void decryptFSTEntryToStream(NodeEntry entry, OutputStream outputStream) throws IOException {
|
||||||
dataProvider.readFileToStream(outputStream, entry);
|
if (!entry.isFile() || entry.isLink()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataProvider.readFileToStream(outputStream, (FileEntry) entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
// Decrypt FSTEntry to OutputStream
|
// Decrypt FSTEntry to OutputStream
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
public void decryptFSTEntryTo(String entryFullPath, OutputStream outputStream) throws IOException, CheckSumWrongException {
|
public void decryptFSTEntryTo(String entryFullPath, OutputStream outputStream) throws IOException, CheckSumWrongException {
|
||||||
FSTEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
|
NodeEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
|
||||||
.orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
|
.orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
decryptFSTEntryToStream(entry, outputStream);
|
decryptFSTEntryToStream(entry, outputStream);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
// Decrypt single FSTEntry to File
|
// Decrypt single FSTEntry to File
|
||||||
@ -132,12 +139,15 @@ public final class DecryptionService {
|
|||||||
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles)
|
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles)
|
||||||
throws IOException, CheckSumWrongException {
|
throws IOException, CheckSumWrongException {
|
||||||
|
|
||||||
FSTEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
|
NodeEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
|
||||||
.orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
|
.orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
|
decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
// Decrypt list of FSTEntry to Files
|
// Decrypt list of FSTEntry to Files
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
@ -156,11 +166,11 @@ public final class DecryptionService {
|
|||||||
decryptFSTEntryListTo(fullPath, FSTUtils.getFSTEntriesByRegEx(dataProvider.getRoot(), regEx), outputFolder, skipExisting);
|
decryptFSTEntryListTo(fullPath, FSTUtils.getFSTEntriesByRegEx(dataProvider.getRoot(), regEx), outputFolder, skipExisting);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decryptFSTEntryListTo(List<FSTEntry> list, String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
|
public void decryptFSTEntryListTo(List<FileEntry> list, String outputFolder, boolean skipExisting) throws IOException, CheckSumWrongException {
|
||||||
decryptFSTEntryListTo(true, list, outputFolder, skipExisting);
|
decryptFSTEntryListTo(true, list, outputFolder, skipExisting);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decryptFSTEntryListTo(boolean fullPath, List<FSTEntry> list, String outputFolder, boolean skipExisting)
|
public void decryptFSTEntryListTo(boolean fullPath, List<FileEntry> list, String outputFolder, boolean skipExisting)
|
||||||
throws IOException, CheckSumWrongException {
|
throws IOException, CheckSumWrongException {
|
||||||
if (parallelizable && Settings.ALLOW_PARALLELISATION) {
|
if (parallelizable && Settings.ALLOW_PARALLELISATION) {
|
||||||
try {
|
try {
|
||||||
@ -175,7 +185,7 @@ public final class DecryptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> decryptFSTEntryListToAsync(boolean fullPath, List<FSTEntry> list, String outputFolder, boolean skipExisting)
|
public CompletableFuture<Void> decryptFSTEntryListToAsync(boolean fullPath, List<FileEntry> list, String outputFolder, boolean skipExisting)
|
||||||
throws IOException, CheckSumWrongException {
|
throws IOException, CheckSumWrongException {
|
||||||
return CompletableFuture
|
return CompletableFuture
|
||||||
.allOf(list.stream().map(entry -> decryptFSTEntryToAsync(fullPath, entry, outputFolder, skipExisting)).toArray(CompletableFuture[]::new));
|
.allOf(list.stream().map(entry -> decryptFSTEntryToAsync(fullPath, entry, outputFolder, skipExisting)).toArray(CompletableFuture[]::new));
|
||||||
|
@ -28,7 +28,7 @@ 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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.interfaces.Parallelizable;
|
import de.mas.wiiu.jnus.interfaces.Parallelizable;
|
||||||
import de.mas.wiiu.jnus.utils.DataProviderUtils;
|
import de.mas.wiiu.jnus.utils.DataProviderUtils;
|
||||||
|
@ -22,10 +22,11 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.TMD.TitleMetaData;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -35,38 +36,38 @@ public class NUSTitle {
|
|||||||
@Getter @Setter private Optional<FST> FST = Optional.empty();
|
@Getter @Setter private Optional<FST> FST = Optional.empty();
|
||||||
@Getter @Setter private Optional<Ticket> ticket;
|
@Getter @Setter private Optional<Ticket> ticket;
|
||||||
|
|
||||||
@Getter private final TMD TMD;
|
@Getter private final TitleMetaData TMD;
|
||||||
|
|
||||||
@Getter private final NUSDataProcessor dataProcessor;
|
@Getter private final NUSDataProcessor dataProcessor;
|
||||||
|
|
||||||
private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) {
|
private NUSTitle(TitleMetaData tmd, NUSDataProcessor dataProcessor) {
|
||||||
this.TMD = tmd;
|
this.TMD = tmd;
|
||||||
this.dataProcessor = dataProcessor;
|
this.dataProcessor = dataProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle create(TMD tmd, NUSDataProcessor dataProcessor, Optional<Ticket> ticket, Optional<FST> fst) {
|
public static NUSTitle create(TitleMetaData tmd, NUSDataProcessor dataProcessor, Optional<Ticket> ticket, Optional<FST> fst) {
|
||||||
NUSTitle result = new NUSTitle(tmd, dataProcessor);
|
NUSTitle result = new NUSTitle(tmd, dataProcessor);
|
||||||
result.setTicket(ticket);
|
result.setTicket(ticket);
|
||||||
result.setFST(fst);
|
result.setFST(fst);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream<FSTEntry> getAllFSTEntriesAsStream() {
|
public Stream<NodeEntry> getAllFSTEntriesAsStream() {
|
||||||
if (!FST.isPresent()) {
|
if (!FST.isPresent()) {
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
return FSTUtils.getAllFSTEntryChildrenAsStream(FST.get().getRoot());
|
return FSTUtils.getAllFSTEntryChildrenAsStream(FST.get().getRootEntry());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FSTEntry> getFSTEntriesByRegEx(String regEx) {
|
public List<FileEntry> getFSTEntriesByRegEx(String regEx) {
|
||||||
return getFSTEntriesByRegEx(regEx, true);
|
return getFSTEntriesByRegEx(regEx, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FSTEntry> getFSTEntriesByRegEx(String regEx, boolean onlyInPackage) {
|
public List<FileEntry> getFSTEntriesByRegEx(String regEx, boolean onlyInPackage) {
|
||||||
if (!FST.isPresent()) {
|
if (!FST.isPresent()) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
return FSTUtils.getFSTEntriesByRegEx(FST.get().getRoot(), regEx, onlyInPackage);
|
return FSTUtils.getFSTEntriesByRegEx(FST.get().getRootEntry(), regEx, onlyInPackage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() throws IOException {
|
public void cleanup() throws IOException {
|
||||||
|
@ -23,11 +23,12 @@ import java.text.ParseException;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
|
import de.mas.wiiu.jnus.entities.TMD.TitleMetaData;
|
||||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
||||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||||
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
|
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
|
||||||
@ -48,7 +49,7 @@ public class NUSTitleLoader {
|
|||||||
throws IOException, ParseException {
|
throws IOException, ParseException {
|
||||||
NUSDataProvider dataProvider = dataProviderFunction.get();
|
NUSDataProvider dataProvider = dataProviderFunction.get();
|
||||||
|
|
||||||
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD().orElseThrow(() -> new FileNotFoundException("No TMD data found")));
|
TitleMetaData tmd = TitleMetaData.parseTMD(dataProvider.getRawTMD().orElseThrow(() -> new FileNotFoundException("No TMD data found")));
|
||||||
|
|
||||||
if (config.isNoDecryption()) {
|
if (config.isNoDecryption()) {
|
||||||
NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(),
|
NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(),
|
||||||
@ -89,8 +90,10 @@ public class NUSTitleLoader {
|
|||||||
NUSTitle result = NUSTitle.create(tmd, dpp, ticket, Optional.empty());
|
NUSTitle result = NUSTitle.create(tmd, dpp, ticket, Optional.empty());
|
||||||
|
|
||||||
FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
|
FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
|
||||||
for (FSTEntry children : dp.getRoot().getChildren()) {
|
for (NodeEntry child : dp.getRoot().getFileChildren()) {
|
||||||
dp.readFile(children);
|
if (!child.isLink()) {
|
||||||
|
dp.readFile((FileEntry) child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -99,7 +102,7 @@ public class NUSTitleLoader {
|
|||||||
Content fstContent = tmd.getContentByIndex(0);
|
Content fstContent = tmd.getContentByIndex(0);
|
||||||
|
|
||||||
byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true);
|
byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true);
|
||||||
FST fst = FST.parseFST(fstBytes);
|
FST fst = FST.parseData(fstBytes);
|
||||||
|
|
||||||
// The dataprovider may need the FST to calculate the offset of a content
|
// The dataprovider may need the FST to calculate the offset of a content
|
||||||
// on the partition.
|
// on the partition.
|
||||||
|
@ -19,7 +19,7 @@ package de.mas.wiiu.jnus;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
|
||||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderFST;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderFST;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
@ -33,7 +33,7 @@ public final class NUSTitleLoaderFST {
|
|||||||
return loadNUSTitle(dataProvider, dataProvider.getRoot(), commonKey);
|
return loadNUSTitle(dataProvider, dataProvider.getRoot(), commonKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(FSTDataProvider dataProvider, FSTEntry base, byte[] commonKey) throws IOException, ParseException {
|
public static NUSTitle loadNUSTitle(FSTDataProvider dataProvider, DirectoryEntry base, byte[] commonKey) throws IOException, ParseException {
|
||||||
NUSTitleConfig config = new NUSTitleConfig();
|
NUSTitleConfig config = new NUSTitleConfig();
|
||||||
config.setCommonKey(commonKey);
|
config.setCommonKey(commonKey);
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import java.text.ParseException;
|
|||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
||||||
|
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
|
||||||
|
|
||||||
public final class NUSTitleLoaderRemote {
|
public final class NUSTitleLoaderRemote {
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ public final class NUSTitleLoaderRemote {
|
|||||||
throw new IOException("Ticket was null and no commonKey was given");
|
throw new IOException("Ticket was null and no commonKey was given");
|
||||||
}
|
}
|
||||||
|
|
||||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID, NUSDownloadService.getDefaultInstance()), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,19 +23,23 @@ import java.nio.file.Files;
|
|||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
||||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
|
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
|
import de.mas.wiiu.jnus.implementations.wud.WiiUDisc;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUDataPartition;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfoParser;
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUGMPartition;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
import lombok.var;
|
||||||
|
|
||||||
public final class WUDLoader {
|
public final class WUDLoader {
|
||||||
|
|
||||||
@ -43,77 +47,88 @@ public final class WUDLoader {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WUDInfo load(String WUDPath) throws IOException, ParseException {
|
public static WiiUDisc load(String WUDPath) throws IOException, ParseException {
|
||||||
return load(WUDPath, (byte[]) null);
|
return load(WUDPath, (byte[]) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WUDInfo load(String WUDPath, File key) throws IOException, ParseException {
|
public static WiiUDisc load(String WUDPath, File key) throws IOException, ParseException {
|
||||||
byte[] data = Files.readAllBytes(key.toPath());
|
byte[] data = Files.readAllBytes(key.toPath());
|
||||||
return load(WUDPath, data);
|
return load(WUDPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WUDInfo loadDev(String WUDPath) throws IOException, ParseException {
|
public static WiiUDisc loadDev(String WUDPath) throws IOException, ParseException {
|
||||||
return load(WUDPath, null, true);
|
return load(WUDPath, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WUDInfo load(String WUDPath, byte[] titleKey) throws IOException, ParseException {
|
public static WiiUDisc load(String WUDPath, byte[] titleKey) throws IOException, ParseException {
|
||||||
return load(WUDPath, titleKey, false);
|
return load(WUDPath, titleKey, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WUDInfo load(String WUDPath, byte[] titleKey, boolean forceNoKey) throws IOException, ParseException {
|
public static WiiUDisc load(String WUDPath, byte[] titleKey, boolean forceNoKey) throws IOException, ParseException {
|
||||||
byte[] usedTitleKey = titleKey;
|
Optional<byte[]> usedTitleKey = Optional.empty();
|
||||||
|
if (titleKey != null) {
|
||||||
|
usedTitleKey = Optional.of(titleKey);
|
||||||
|
}
|
||||||
File wudFile = new File(WUDPath);
|
File wudFile = new File(WUDPath);
|
||||||
if (!wudFile.exists()) {
|
if (!wudFile.exists()) {
|
||||||
throw new FileNotFoundException(wudFile.getAbsolutePath() + " was not found");
|
throw new FileNotFoundException(wudFile.getAbsolutePath() + " was not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
WUDImage image = new WUDImage(wudFile);
|
WUDImage image = new WUDImage(wudFile);
|
||||||
if (usedTitleKey == null && !forceNoKey) {
|
if (titleKey == null && !forceNoKey) {
|
||||||
File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
|
File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
|
||||||
if (!keyFile.exists()) {
|
if (!keyFile.exists()) {
|
||||||
throw new FileNotFoundException(keyFile.getAbsolutePath() + " does not exist and no title key was provided.");
|
throw new FileNotFoundException(keyFile.getAbsolutePath() + " does not exist and no title key was provided.");
|
||||||
}
|
}
|
||||||
usedTitleKey = Files.readAllBytes(keyFile.toPath());
|
usedTitleKey = Optional.of(Files.readAllBytes(keyFile.toPath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), usedTitleKey);
|
WiiUDisc wiiUDisc = WiiUDisc.parseData(image.getWUDDiscReader(), usedTitleKey);
|
||||||
|
|
||||||
return wudInfo;
|
return wiiUDisc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<NUSTitle> getGamePartionsAsNUSTitles(@NonNull WUDInfo wudInfo, byte[] commonKey) throws IOException, ParseException {
|
public static List<NUSTitle> getGamePartionsAsNUSTitles(@NonNull WiiUDisc disc, byte[] commonKey) throws IOException, ParseException {
|
||||||
List<NUSTitle> result = new ArrayList<>();
|
List<NUSTitle> result = new ArrayList<>();
|
||||||
|
|
||||||
for (val gamePartition : wudInfo.getGamePartitions()) {
|
List<WiiUGMPartition> gamePartitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUGMPartition)
|
||||||
result.add(convertGamePartitionToNUSTitle(gamePartition, wudInfo.getWUDDiscReader(), commonKey));
|
.map(p -> (WiiUGMPartition) p).collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (val gamePartition : gamePartitions) {
|
||||||
|
result.add(convertGamePartitionToNUSTitle(gamePartition, disc.getReader().get(), commonKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle convertGamePartitionToNUSTitle(WUDGamePartition gamePartition, WUDDiscReader discReader, byte[] commonKey)
|
public static NUSTitle convertGamePartitionToNUSTitle(WiiUGMPartition gamePartition, WUDDiscReader discReader, byte[] commonKey)
|
||||||
throws IOException, ParseException {
|
throws IOException, ParseException {
|
||||||
final NUSTitleConfig config = new NUSTitleConfig();
|
final NUSTitleConfig config = new NUSTitleConfig();
|
||||||
config.setCommonKey(commonKey);
|
config.setCommonKey(commonKey);
|
||||||
gamePartition.getTmd();
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader),
|
||||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
(dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WUDInfo wudInfo, byte[] commonKey) throws IOException, ParseException {
|
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WiiUDisc disc, byte[] commonKey) throws IOException, ParseException {
|
||||||
List<FSTDataProvider> result = new ArrayList<>();
|
List<FSTDataProvider> result = new ArrayList<>();
|
||||||
for (val gamePartition : wudInfo.getGamePartitions()) {
|
List<WiiUGMPartition> gamePartitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUGMPartition)
|
||||||
NUSTitle t = convertGamePartitionToNUSTitle(gamePartition, wudInfo.getWUDDiscReader(), commonKey);
|
.map(p -> (WiiUGMPartition) p).collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (val gamePartition : gamePartitions) {
|
||||||
|
NUSTitle t = convertGamePartitionToNUSTitle(gamePartition, disc.getReader().get(), commonKey);
|
||||||
FSTDataProviderNUSTitle res = new FSTDataProviderNUSTitle(t);
|
FSTDataProviderNUSTitle res = new FSTDataProviderNUSTitle(t);
|
||||||
res.setName(gamePartition.getPartitionName());
|
res.setName(gamePartition.getVolumeID());
|
||||||
result.add(res);
|
result.add(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (val partition : wudInfo.getDataPartitions()) {
|
List<WiiUDataPartition> dataParitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUDataPartition)
|
||||||
result.add(new FSTDataProviderWUDDataPartition(partition, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey()));
|
.map(p -> ((WiiUDataPartition) p)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (val partition : dataParitions) {
|
||||||
|
result.add(new FSTDataProviderWUDDataPartition(partition, disc.getReader().get(), disc.getDiscKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,8 @@ public class WumadLoader {
|
|||||||
final NUSTitleConfig config = new NUSTitleConfig();
|
final NUSTitleConfig config = new NUSTitleConfig();
|
||||||
config.setCommonKey(commonKey);
|
config.setCommonKey(commonKey);
|
||||||
gamePartition.getTmd();
|
gamePartition.getTmd();
|
||||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile), (dp, cd, ce) -> new DefaultNUSDataProcessor(dp, cd));
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile),
|
||||||
|
(dp, cd, ce) -> new DefaultNUSDataProcessor(dp, cd));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException {
|
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* 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.entities.content;
|
package de.mas.wiiu.jnus.entities.TMD;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
@ -63,7 +63,7 @@ public class Content implements Comparable<Content> {
|
|||||||
* @return content object
|
* @return content object
|
||||||
* @throws ParseException
|
* @throws ParseException
|
||||||
*/
|
*/
|
||||||
public static Content parseContent(byte[] input) throws ParseException {
|
public static Content parseData(byte[] input) throws ParseException {
|
||||||
if (input == null || input.length != CONTENT_SIZE) {
|
if (input == null || input.length != CONTENT_SIZE) {
|
||||||
log.info("Error: invalid Content byte[] input");
|
log.info("Error: invalid Content byte[] input");
|
||||||
throw new ParseException("Error: invalid Content byte[] input", 0);
|
throw new ParseException("Error: invalid Content byte[] input", 0);
|
||||||
@ -180,8 +180,6 @@ public class Content implements Comparable<Content> {
|
|||||||
|
|
||||||
private long encryptedFileSize;
|
private long encryptedFileSize;
|
||||||
private byte[] SHA2Hash;
|
private byte[] SHA2Hash;
|
||||||
|
|
||||||
private ContentFSTInfo contentFSTInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
@ -14,7 +14,7 @@
|
|||||||
* 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.entities.content;
|
package de.mas.wiiu.jnus.entities.TMD;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
@ -69,7 +69,7 @@ public class ContentInfo {
|
|||||||
* @return ContentFSTInfo object
|
* @return ContentFSTInfo object
|
||||||
* @throws ParseException
|
* @throws ParseException
|
||||||
*/
|
*/
|
||||||
public static ContentInfo parseContentInfo(byte[] input) throws ParseException {
|
public static ContentInfo parseData(byte[] input) throws ParseException {
|
||||||
if (input == null || input.length != CONTENT_INFO_SIZE) {
|
if (input == null || input.length != CONTENT_INFO_SIZE) {
|
||||||
log.info("Error: invalid ContentInfo byte[] input");
|
log.info("Error: invalid ContentInfo byte[] input");
|
||||||
throw new ParseException("Error: invalid ContentInfo byte[] input", 0);
|
throw new ParseException("Error: invalid ContentInfo byte[] input", 0);
|
@ -14,7 +14,7 @@
|
|||||||
* 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.entities;
|
package de.mas.wiiu.jnus.entities.TMD;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -27,14 +27,12 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentInfo;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.java.Log;
|
import lombok.extern.java.Log;
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public final class TMD {
|
public final class TitleMetaData {
|
||||||
private static final int SIGNATURE_LENGTH = 0x100;
|
private static final int SIGNATURE_LENGTH = 0x100;
|
||||||
private static final int ISSUER_LENGTH = 0x40;
|
private static final int ISSUER_LENGTH = 0x40;
|
||||||
private static final int RESERVED_LENGTH = 0x3E;
|
private static final int RESERVED_LENGTH = 0x3E;
|
||||||
@ -78,7 +76,7 @@ public final class TMD {
|
|||||||
private final Map<Integer, Content> contentToIndex = new HashMap<>();
|
private final Map<Integer, Content> contentToIndex = new HashMap<>();
|
||||||
private final Map<Integer, Content> contentToID = new HashMap<>();
|
private final Map<Integer, Content> contentToID = new HashMap<>();
|
||||||
|
|
||||||
private TMD(TMDParam param) {
|
private TitleMetaData(TMDParam param) {
|
||||||
super();
|
super();
|
||||||
this.signatureType = param.getSignatureType();
|
this.signatureType = param.getSignatureType();
|
||||||
this.signature = param.getSignature();
|
this.signature = param.getSignature();
|
||||||
@ -101,7 +99,7 @@ public final class TMD {
|
|||||||
this.cert2 = param.getCert2();
|
this.cert2 = param.getCert2();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TMD parseTMD(File tmd) throws IOException, ParseException {
|
public static TitleMetaData parseTMD(File tmd) throws IOException, ParseException {
|
||||||
if (tmd == null || !tmd.exists()) {
|
if (tmd == null || !tmd.exists()) {
|
||||||
log.info("TMD input file null or doesn't exist.");
|
log.info("TMD input file null or doesn't exist.");
|
||||||
throw new IOException("TMD input file null or doesn't exist.");
|
throw new IOException("TMD input file null or doesn't exist.");
|
||||||
@ -109,7 +107,7 @@ public final class TMD {
|
|||||||
return parseTMD(Files.readAllBytes(tmd.toPath()));
|
return parseTMD(Files.readAllBytes(tmd.toPath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TMD parseTMD(byte[] input) throws ParseException {
|
public static TitleMetaData parseTMD(byte[] input) throws ParseException {
|
||||||
if (input == null || input.length == 0) {
|
if (input == null || input.length == 0) {
|
||||||
throw new ParseException("Invalid TMD file.", 0);
|
throw new ParseException("Invalid TMD file.", 0);
|
||||||
}
|
}
|
||||||
@ -171,7 +169,7 @@ public final class TMD {
|
|||||||
for (int i = 0; i < CONTENT_INFO_ARRAY_SIZE; i++) {
|
for (int i = 0; i < CONTENT_INFO_ARRAY_SIZE; i++) {
|
||||||
byte[] contentInfo = new byte[ContentInfo.CONTENT_INFO_SIZE];
|
byte[] contentInfo = new byte[ContentInfo.CONTENT_INFO_SIZE];
|
||||||
buffer.get(contentInfo, 0, ContentInfo.CONTENT_INFO_SIZE);
|
buffer.get(contentInfo, 0, ContentInfo.CONTENT_INFO_SIZE);
|
||||||
contentInfos[i] = ContentInfo.parseContentInfo(contentInfo);
|
contentInfos[i] = ContentInfo.parseData(contentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Content> contentList = new ArrayList<>();
|
List<Content> contentList = new ArrayList<>();
|
||||||
@ -180,7 +178,7 @@ public final class TMD {
|
|||||||
buffer.position(CONTENT_OFFSET + (Content.CONTENT_SIZE * i));
|
buffer.position(CONTENT_OFFSET + (Content.CONTENT_SIZE * i));
|
||||||
byte[] content = new byte[Content.CONTENT_SIZE];
|
byte[] content = new byte[Content.CONTENT_SIZE];
|
||||||
buffer.get(content, 0, Content.CONTENT_SIZE);
|
buffer.get(content, 0, Content.CONTENT_SIZE);
|
||||||
Content c = Content.parseContent(content);
|
Content c = Content.parseData(content);
|
||||||
contentList.add(c);
|
contentList.add(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +211,7 @@ public final class TMD {
|
|||||||
param.setCert1(cert1);
|
param.setCert1(cert1);
|
||||||
param.setCert2(cert2);
|
param.setCert2(cert2);
|
||||||
|
|
||||||
TMD result = new TMD(param);
|
TitleMetaData result = new TitleMetaData(param);
|
||||||
|
|
||||||
for (Content c : contentList) {
|
for (Content c : contentList) {
|
||||||
result.setContentToIndex(c.getIndex(), c);
|
result.setContentToIndex(c.getIndex(), c);
|
@ -1,122 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.entities.content;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.text.ParseException;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.java.Log;
|
|
||||||
|
|
||||||
@EqualsAndHashCode
|
|
||||||
/**
|
|
||||||
* Representation on an Object of the first section of an FST.
|
|
||||||
*
|
|
||||||
* @author Maschell
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Log
|
|
||||||
public final class ContentFSTInfo {
|
|
||||||
@Getter private final long offsetSector;
|
|
||||||
@Getter private final long sizeSector;
|
|
||||||
@Getter private final long ownerTitleID;
|
|
||||||
@Getter private final int groupID;
|
|
||||||
@Getter private final byte unkown;
|
|
||||||
|
|
||||||
private static int SECTOR_SIZE = 0x8000;
|
|
||||||
|
|
||||||
private ContentFSTInfo(ContentFSTInfoParam param) {
|
|
||||||
this.offsetSector = param.getOffsetSector();
|
|
||||||
this.sizeSector = param.getSizeSector();
|
|
||||||
this.ownerTitleID = param.getOwnerTitleID();
|
|
||||||
this.groupID = param.getGroupID();
|
|
||||||
this.unkown = param.getUnkown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ContentFSTInfo object given be the raw byte data
|
|
||||||
*
|
|
||||||
* @param input
|
|
||||||
* 0x20 byte of data from the FST (starting at 0x20)
|
|
||||||
* @return ContentFSTInfo object
|
|
||||||
* @throws ParseException
|
|
||||||
*/
|
|
||||||
public static ContentFSTInfo parseContentFST(byte[] input) throws ParseException {
|
|
||||||
if (input == null || input.length != 0x20) {
|
|
||||||
log.info("Error: invalid ContentFSTInfo byte[] input");
|
|
||||||
throw new ParseException("Failed to parse ContentFSTInfo", 0);
|
|
||||||
}
|
|
||||||
ContentFSTInfoParam param = new ContentFSTInfoParam();
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
|
||||||
buffer.put(input);
|
|
||||||
|
|
||||||
buffer.position(0);
|
|
||||||
int offset = buffer.getInt();
|
|
||||||
int size = buffer.getInt();
|
|
||||||
long ownerTitleID = buffer.getLong();
|
|
||||||
int groupID = buffer.getInt();
|
|
||||||
byte unkown = buffer.get();
|
|
||||||
|
|
||||||
param.setOffsetSector(offset);
|
|
||||||
param.setSizeSector(size);
|
|
||||||
param.setOwnerTitleID(ownerTitleID);
|
|
||||||
param.setGroupID(groupID);
|
|
||||||
param.setUnkown(unkown);
|
|
||||||
|
|
||||||
return new ContentFSTInfo(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the offset of of the Content in the partition
|
|
||||||
*
|
|
||||||
* @return offset of the content in the partition in bytes
|
|
||||||
*/
|
|
||||||
public long getOffset() {
|
|
||||||
long result = (getOffsetSector() * SECTOR_SIZE) - SECTOR_SIZE;
|
|
||||||
if (result < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size in bytes, not in sectors
|
|
||||||
*
|
|
||||||
* @return size in bytes
|
|
||||||
*/
|
|
||||||
public int getSize() {
|
|
||||||
return (int) (getSizeSector() * SECTOR_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ContentFSTInfo [offset=" + String.format("%08X", offsetSector) + ", size=" + String.format("%08X", sizeSector) + ", ownerTitleID="
|
|
||||||
+ String.format("%016X", ownerTitleID) + ", groupID=" + String.format("%08X", groupID) + ", unkown=" + unkown + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class ContentFSTInfoParam {
|
|
||||||
private long offsetSector;
|
|
||||||
private long sizeSector;
|
|
||||||
private long ownerTitleID;
|
|
||||||
private int groupID;
|
|
||||||
private byte unkown;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,97 +1,205 @@
|
|||||||
/****************************************************************************
|
package de.mas.wiiu.jnus.entities.FST;
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.entities.fst;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.LinkedList;
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
import de.mas.wiiu.jnus.entities.FST.header.Header;
|
||||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
|
||||||
import lombok.Getter;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.Permission;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionBlockSize;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SizeInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.VolumeBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
/**
|
@Data
|
||||||
* Represents the FST
|
public class FST {
|
||||||
*
|
private Header header;
|
||||||
* @author Maschell
|
private SectionEntries sectionEntries;
|
||||||
*
|
private StringTable stringTable;
|
||||||
*/
|
private NodeEntries nodeEntries;
|
||||||
public final class FST {
|
|
||||||
@Getter private final FSTEntry root = FSTEntry.getRootFSTEntry();
|
|
||||||
|
|
||||||
@Getter private final int sectorSize;
|
public static FST parseData(byte[] data) throws IOException {
|
||||||
@Getter private final int contentCount;
|
return parseData(data, 0, new VolumeBlockSize(1));
|
||||||
|
|
||||||
@Getter private final Map<Integer, ContentFSTInfo> contentFSTInfos = new HashMap<>();
|
|
||||||
|
|
||||||
private FST(int unknown, int contentCount) {
|
|
||||||
this.sectorSize = unknown;
|
|
||||||
this.contentCount = contentCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static FST parseData(byte[] data, int offset, VolumeBlockSize blockSize) throws IOException {
|
||||||
* Creates a FST by the given raw byte data
|
FST fst = new FST();
|
||||||
*
|
|
||||||
* @param fstData
|
int curOffset = offset;
|
||||||
* raw decrypted FST data
|
|
||||||
* @return
|
fst.header = Header.parseData(Arrays.copyOfRange(data, curOffset, Header.LENGTH), 0);
|
||||||
* @throws ParseException
|
curOffset += Header.LENGTH;
|
||||||
*/
|
fst.sectionEntries = SectionEntries.parseData(data, curOffset, fst.header.getNumberOfSections(), blockSize);
|
||||||
public static FST parseFST(byte[] fstData) throws ParseException {
|
curOffset += fst.sectionEntries.getSizeInBytes();
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[] { 0x46, 0x53, 0x54 })) {
|
int lastEntryNumber = RootEntry.parseLastEntryNumber(data, curOffset);
|
||||||
throw new ParseException("Failed to parse FST", 0);
|
|
||||||
|
fst.stringTable = StringTable.parseData(data, curOffset + (lastEntryNumber * 16), lastEntryNumber);
|
||||||
|
fst.nodeEntries = NodeEntries.parseData(data, curOffset, fst.sectionEntries, fst.stringTable, fst.header.getBlockSize());
|
||||||
|
|
||||||
|
return fst;
|
||||||
}
|
}
|
||||||
|
|
||||||
int sectorSize = ByteUtils.getIntFromBytes(fstData, 0x04);
|
public RootEntry getRootEntry() {
|
||||||
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
|
return getNodeEntries().getRootEntry();
|
||||||
|
|
||||||
FST result = new FST(sectorSize, contentCount);
|
|
||||||
|
|
||||||
int contentfst_offset = 0x20;
|
|
||||||
int contentfst_size = 0x20 * contentCount;
|
|
||||||
|
|
||||||
int fst_offset = contentfst_offset + contentfst_size;
|
|
||||||
|
|
||||||
int fileCount = ByteUtils.getIntFromBytes(fstData, fst_offset + 0x08);
|
|
||||||
int fst_size = fileCount * 0x10;
|
|
||||||
|
|
||||||
int nameOff = fst_offset + fst_size;
|
|
||||||
int nameSize = fstData.length - nameOff;
|
|
||||||
|
|
||||||
// Get list with null-terminated Strings. Ends with \0\0.
|
|
||||||
for (int i = nameOff; i < fstData.length - 1; i++) {
|
|
||||||
if (fstData[i] == 0 && fstData[i + 1] == 0) {
|
|
||||||
nameSize = i - nameOff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Integer, ContentFSTInfo> contentFSTInfos = result.getContentFSTInfos();
|
public byte[] getAsBytes() {
|
||||||
for (int i = 0; i < contentCount; i++) {
|
byte[] headerData = header.getAsBytes();
|
||||||
byte contentFST[] = Arrays.copyOfRange(fstData, contentfst_offset + (i * 0x20), contentfst_offset + ((i + 1) * 0x20));
|
byte[] sectionData = sectionEntries.getAsBytes();
|
||||||
contentFSTInfos.put(i, ContentFSTInfo.parseContentFST(contentFST));
|
byte[] stringTableData = stringTable.getAsBytes();
|
||||||
|
byte[] nodeEntriesData = nodeEntries.getAsBytes();
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(headerData.length + sectionData.length + stringTableData.length + nodeEntriesData.length);
|
||||||
|
buffer.put(headerData);
|
||||||
|
buffer.put(sectionData);
|
||||||
|
buffer.put(nodeEntriesData);
|
||||||
|
buffer.put(stringTableData);
|
||||||
|
return buffer.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size);
|
public static FST createFromFolder(File folder) {
|
||||||
byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize);
|
Header header = new Header();
|
||||||
|
header.setFSTVersion((short) 0);
|
||||||
|
header.setHashDisabled((short) 1);
|
||||||
|
header.setBlockSize(new SectionBlockSize(32));
|
||||||
|
|
||||||
FSTEntry root = result.getRoot();
|
VolumeBlockSize blockSize = new VolumeBlockSize(32768);
|
||||||
|
|
||||||
FSTService.parseFST(root, fstSection, nameSection, sectorSize);
|
StringTable stringtable = new StringTable(getStringsFromFolder(folder));
|
||||||
|
|
||||||
|
SectionEntry fstSection = new SectionEntry(0, "FST");
|
||||||
|
fstSection.setAddress(new AddressInVolumeBlocks(blockSize, 0L));
|
||||||
|
// for some reason this is always 0. We still need to adjust the offset of the next sections properly though.
|
||||||
|
fstSection.setSize(new SizeInVolumeBlocks(blockSize, 0L));
|
||||||
|
fstSection.setHashMode((short) 0);
|
||||||
|
|
||||||
|
SectionEntry codeSection = new SectionEntry(1, "CODE");
|
||||||
|
// Add real FST Size.
|
||||||
|
codeSection.setAddress(new AddressInVolumeBlocks(blockSize, 0L));
|
||||||
|
codeSection.setHashMode((short) 1);
|
||||||
|
|
||||||
|
SectionEntry filesSection = new SectionEntry(2, "FILES");
|
||||||
|
// Add real FST Size.
|
||||||
|
filesSection.setAddress(new AddressInVolumeBlocks(blockSize, 0L));
|
||||||
|
filesSection.setHashMode((short) 2);
|
||||||
|
|
||||||
|
SectionEntries sections = new SectionEntries();
|
||||||
|
|
||||||
|
sections.add(fstSection);
|
||||||
|
sections.add(codeSection);
|
||||||
|
sections.add(filesSection);
|
||||||
|
|
||||||
|
DirectoryEntry dir = new DirectoryEntry();
|
||||||
|
dir.setEntryType(new EntryType(EEntryType.Directory));
|
||||||
|
dir.setParent(null);
|
||||||
|
dir.setEntryNumber(0);
|
||||||
|
dir.setNameString(stringtable.getEntry("").get());
|
||||||
|
dir.setPermission(new Permission(0));
|
||||||
|
dir.setSectionEntry(fstSection);
|
||||||
|
|
||||||
|
RootEntry root = new RootEntry(dir);
|
||||||
|
|
||||||
|
// Split files per folder. the /code folder seems to have an own section?
|
||||||
|
File[] codeFolder = folder.listFiles(f -> f.isDirectory() && "code".contentEquals(f.getName()));
|
||||||
|
if (codeFolder.length > 0) {
|
||||||
|
NodeEntry res = NodeEntries.createFromFolder(codeFolder[0], header.getBlockSize(), stringtable, codeSection, new Permission(0));
|
||||||
|
root.addChild(res);
|
||||||
|
// res.printToStringRecursive();
|
||||||
|
codeSection.setSize(calculateSectionSizeByFSTEntries((int) blockSize.getBlockSize(), res));
|
||||||
|
}
|
||||||
|
|
||||||
|
File[] otherFolder = folder.listFiles(f -> f.isDirectory() && !"code".contentEquals(f.getName()));
|
||||||
|
for (val curFolder : otherFolder) {
|
||||||
|
NodeEntry res = NodeEntries.createFromFolder(curFolder, header.getBlockSize(), stringtable, filesSection, new Permission(0));
|
||||||
|
root.addChild(res);
|
||||||
|
// res.printToStringRecursive();
|
||||||
|
filesSection.setSize(calculateSectionSizeByFSTEntries((int) blockSize.getBlockSize(), res));
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntryNumbers(root, 0);
|
||||||
|
|
||||||
|
// root.printToStringRecursive();
|
||||||
|
|
||||||
|
NodeEntries nodeEntries = new NodeEntries(root);
|
||||||
|
|
||||||
|
header.setNumberOfSections(sections.size());
|
||||||
|
|
||||||
|
FST fst = new FST();
|
||||||
|
fst.setHeader(header);
|
||||||
|
fst.setSectionEntries(sections);
|
||||||
|
fst.setStringTable(stringtable);
|
||||||
|
fst.setNodeEntries(nodeEntries);
|
||||||
|
|
||||||
|
// Fix FST section length
|
||||||
|
// int length = fst.getAsBytes().length;
|
||||||
|
// fstSection.setSize(new VolumeLbaSize(blockSize, (length / blockSize.getBlockSize())+1));
|
||||||
|
|
||||||
|
long sectionAddressInBlocks = 2;
|
||||||
|
for (SectionEntry s : sections) {
|
||||||
|
if (s.getSectionNumber() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s.setAddress(new AddressInVolumeBlocks(blockSize, sectionAddressInBlocks));
|
||||||
|
sectionAddressInBlocks += s.getSize().getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fst;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SizeInVolumeBlocks calculateSectionSizeByFSTEntries(int blockSize, NodeEntry input) {
|
||||||
|
// calculate size of section.
|
||||||
|
Optional<FileEntry> lastEntry = input.stream().filter(e -> e instanceof FileEntry).map(f -> (FileEntry) f)
|
||||||
|
.max((a, b) -> Long.compare(a.getOffset(), b.getOffset()));
|
||||||
|
if (!lastEntry.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("WTF?");
|
||||||
|
}
|
||||||
|
long sectionSize = lastEntry.get().getOffset() + lastEntry.get().getSize();
|
||||||
|
sectionSize = Utils.align(sectionSize, blockSize);
|
||||||
|
int sectionSizeInBlocks = (int) (sectionSize / blockSize);
|
||||||
|
sectionSizeInBlocks = sectionSize % blockSize == 0 ? sectionSizeInBlocks : sectionSizeInBlocks + 1;
|
||||||
|
return new SizeInVolumeBlocks(new VolumeBlockSize(blockSize), (long) sectionSizeInBlocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int setEntryNumbers(NodeEntry entry, int curEntryNumber) {
|
||||||
|
entry.setEntryNumber(curEntryNumber);
|
||||||
|
curEntryNumber++;
|
||||||
|
if (entry instanceof DirectoryEntry) {
|
||||||
|
for (NodeEntry child : ((DirectoryEntry) entry).getChildren()) {
|
||||||
|
curEntryNumber = setEntryNumbers(child, curEntryNumber);
|
||||||
|
}
|
||||||
|
((DirectoryEntry) entry).setLastEntryNumber(curEntryNumber);
|
||||||
|
((DirectoryEntry) entry).setParentEntryNumber(entry.getEntryNumber());
|
||||||
|
}
|
||||||
|
return curEntryNumber;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getStringsFromFolder(File input) {
|
||||||
|
List<String> result = new LinkedList<>();
|
||||||
|
|
||||||
|
for (File f : input.listFiles()) {
|
||||||
|
result.add(f.getName());
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
result.addAll(getStringsFromFolder(f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,200 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.entities.fst;
|
|
||||||
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NonNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents one FST Entry
|
|
||||||
*
|
|
||||||
* @author Maschell
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class FSTEntry {
|
|
||||||
public static final byte FSTEntry_DIR = (byte) 0x01;
|
|
||||||
public static final byte FSTEntry_notInNUS = (byte) 0x80;
|
|
||||||
|
|
||||||
private String filename = null;
|
|
||||||
private final Supplier<String> filenameSupplier;
|
|
||||||
|
|
||||||
@Getter private final Optional<FSTEntry> parent;
|
|
||||||
|
|
||||||
@Getter private final List<FSTEntry> children = new ArrayList<>();
|
|
||||||
|
|
||||||
@Getter private final short flags;
|
|
||||||
|
|
||||||
@Getter private final long fileSize;
|
|
||||||
@Getter private final long fileOffset;
|
|
||||||
|
|
||||||
@Getter private final boolean isDir;
|
|
||||||
@Getter private final boolean isRoot;
|
|
||||||
@Getter private final boolean isNotInPackage;
|
|
||||||
|
|
||||||
@Getter private final short contentIndex;
|
|
||||||
|
|
||||||
protected FSTEntry(FSTEntryParam fstParam) {
|
|
||||||
this.filenameSupplier = fstParam.getFileNameSupplier();
|
|
||||||
this.flags = fstParam.getFlags();
|
|
||||||
this.parent = fstParam.getParent();
|
|
||||||
|
|
||||||
this.fileSize = fstParam.getFileSize();
|
|
||||||
this.fileOffset = fstParam.getFileOffset();
|
|
||||||
|
|
||||||
this.isDir = fstParam.isDir();
|
|
||||||
this.isRoot = fstParam.isRoot();
|
|
||||||
this.isNotInPackage = fstParam.isNotInPackage();
|
|
||||||
this.contentIndex = fstParam.getContentIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a new FST Entry
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static FSTEntry getRootFSTEntry() {
|
|
||||||
FSTEntryParam param = new FSTEntryParam();
|
|
||||||
param.setRoot(true);
|
|
||||||
param.setDir(true);
|
|
||||||
return new FSTEntry(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FSTEntry createFSTEntry(@NonNull FSTEntry parent, @NonNull String filename, @NonNull Content content) {
|
|
||||||
FSTEntryParam param = new FSTEntryParam();
|
|
||||||
param.setFileNameSupplier(() -> filename);
|
|
||||||
param.setFileSize(content.getDecryptedFileSize());
|
|
||||||
param.setDir(false);
|
|
||||||
param.setParent(Optional.of(parent));
|
|
||||||
return new FSTEntry(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename() {
|
|
||||||
if (filename == null) {
|
|
||||||
filename = filenameSupplier.get();
|
|
||||||
}
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFullPath() {
|
|
||||||
return getPath() + getFilename();
|
|
||||||
}
|
|
||||||
|
|
||||||
private StringBuilder getPathInternal() {
|
|
||||||
if (parent.isPresent()) {
|
|
||||||
FSTEntry par = parent.get();
|
|
||||||
return par.getPathInternal().append(par.getFilename()).append('/');
|
|
||||||
}
|
|
||||||
return new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPath() {
|
|
||||||
return getPathInternal().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getEntryCount() {
|
|
||||||
int count = 1;
|
|
||||||
for (FSTEntry entry : getChildren()) {
|
|
||||||
count += entry.getEntryCount();
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FSTEntry> getDirChildren() {
|
|
||||||
return getDirChildren(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FSTEntry> getDirChildren(boolean all) {
|
|
||||||
List<FSTEntry> result = new ArrayList<>();
|
|
||||||
for (FSTEntry child : getChildren()) {
|
|
||||||
if (child.isDir() && (all || !child.isNotInPackage())) {
|
|
||||||
result.add(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FSTEntry> getFileChildren() {
|
|
||||||
return getFileChildren(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FSTEntry> getFileChildren(boolean all) {
|
|
||||||
List<FSTEntry> result = new ArrayList<>();
|
|
||||||
for (FSTEntry child : getChildren()) {
|
|
||||||
if ((all && !child.isDir() || !child.isDir())) {
|
|
||||||
result.add(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void printRecursive(int space) {
|
|
||||||
printRecursive(System.out, space);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void printRecursive(PrintStream out, int space) {
|
|
||||||
for (int i = 0; i < space; i++) {
|
|
||||||
out.print(" ");
|
|
||||||
}
|
|
||||||
out.print(getFilename());
|
|
||||||
if (isNotInPackage()) {
|
|
||||||
out.print(" (not in package)");
|
|
||||||
}
|
|
||||||
out.println();
|
|
||||||
for (FSTEntry child : getDirChildren(true)) {
|
|
||||||
child.printRecursive(space + 5);
|
|
||||||
}
|
|
||||||
for (FSTEntry child : getFileChildren(true)) {
|
|
||||||
child.printRecursive(space + 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "FSTEntry [filename=" + getFilename() + ", path=" + getPath() + ", flags=" + flags + ", filesize=" + fileSize + ", fileoffset=" + fileOffset
|
|
||||||
+ ", isDir=" + isDir + ", isRoot=" + isRoot + ", notInPackage=" + isNotInPackage + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
protected static class FSTEntryParam {
|
|
||||||
private Supplier<String> fileNameSupplier = () -> "";
|
|
||||||
private Optional<FSTEntry> parent = Optional.empty();
|
|
||||||
|
|
||||||
private short flags;
|
|
||||||
|
|
||||||
private long fileSize = 0;
|
|
||||||
private long fileOffset = 0;
|
|
||||||
|
|
||||||
private boolean isDir = false;
|
|
||||||
private boolean isRoot = false;
|
|
||||||
private boolean notInPackage = false;
|
|
||||||
|
|
||||||
private short contentIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addChildren(FSTEntry entry) {
|
|
||||||
this.getChildren().add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.entities.fst;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry.FSTEntryParam;
|
|
||||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
|
||||||
|
|
||||||
public final class FSTService {
|
|
||||||
|
|
||||||
private FSTService() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection, int sectorSize) throws ParseException {
|
|
||||||
int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
|
|
||||||
|
|
||||||
final Map<Integer, byte[]> data = new HashMap<>();
|
|
||||||
|
|
||||||
for (int i = 1; i < totalEntries; i++) {
|
|
||||||
data.put(i, Arrays.copyOfRange(fstSection, i * 0x10, (i + 1) * 0x10));
|
|
||||||
}
|
|
||||||
|
|
||||||
parseData(1, totalEntries, rootEntry, data, namesSection, sectorSize);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int parseData(int i, int end, FSTEntry parent, Map<Integer, byte[]> data, byte[] namesSection, int sectorSize) throws ParseException {
|
|
||||||
while (i < end) {
|
|
||||||
byte[] curEntry = data.get(i);
|
|
||||||
|
|
||||||
FSTEntryParam entryParam = new FSTEntry.FSTEntryParam();
|
|
||||||
|
|
||||||
entryParam.setParent(Optional.of(parent));
|
|
||||||
|
|
||||||
long fileOffset = ByteUtils.getIntFromBytes(curEntry, 0x04);
|
|
||||||
long fileSize = ByteUtils.getUnsingedIntFromBytes(curEntry, 0x08);
|
|
||||||
short flags = ByteUtils.getShortFromBytes(curEntry, 0x0C);
|
|
||||||
short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
|
|
||||||
|
|
||||||
if ((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS) {
|
|
||||||
entryParam.setNotInPackage(true);
|
|
||||||
}
|
|
||||||
if ((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR) {
|
|
||||||
entryParam.setDir(true);
|
|
||||||
} else {
|
|
||||||
entryParam.setFileOffset(fileOffset * sectorSize);
|
|
||||||
entryParam.setFileSize(fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
entryParam.setFlags(flags);
|
|
||||||
entryParam.setContentIndex(contentIndex);
|
|
||||||
|
|
||||||
final int nameOffset = getNameOffset(curEntry);
|
|
||||||
entryParam.setFileNameSupplier(() -> getName(nameOffset, namesSection));
|
|
||||||
|
|
||||||
FSTEntry entry = new FSTEntry(entryParam);
|
|
||||||
|
|
||||||
parent.addChildren(entry);
|
|
||||||
|
|
||||||
if (entryParam.isDir()) {
|
|
||||||
i = parseData(i + 1, (int) fileSize, entry, data, namesSection, sectorSize);
|
|
||||||
} else {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getNameOffset(byte[] curEntry) {
|
|
||||||
// Its a 24bit number. We overwrite the first byte, then we can read it as an Integer.
|
|
||||||
// But at first we make a copy.
|
|
||||||
byte[] entryData = Arrays.copyOf(curEntry, curEntry.length);
|
|
||||||
entryData[0] = 0;
|
|
||||||
return ByteUtils.getIntFromBytes(entryData, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getName(byte[] data, byte[] namesSection) {
|
|
||||||
return getName(getNameOffset(data), namesSection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getName(int nameOffset, byte[] namesSection) {
|
|
||||||
int j = 0;
|
|
||||||
|
|
||||||
while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) {
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.header;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Header {
|
||||||
|
public static String MAGIC = "FST";
|
||||||
|
public static int LENGTH = 32;
|
||||||
|
private short FSTVersion;
|
||||||
|
private SectionBlockSize blockSize;
|
||||||
|
private long numberOfSections;
|
||||||
|
private short hashDisabled;
|
||||||
|
|
||||||
|
public static Header parseData(byte[] data, long offset) throws IOException {
|
||||||
|
Header header = new Header();
|
||||||
|
|
||||||
|
byte[] strRaw = Arrays.copyOfRange(data, 0, 3);
|
||||||
|
String compareMagic = new String(strRaw, Charset.forName("ISO-8859-1"));
|
||||||
|
if (!MAGIC.equals(compareMagic)) {
|
||||||
|
throw new IOException("FST Header magic was wrong");
|
||||||
|
}
|
||||||
|
header.FSTVersion = ByteUtils.getByteFromBytes(data, 3);
|
||||||
|
header.blockSize = new SectionBlockSize(ByteUtils.getUnsingedIntFromBytes(data, 4) & 0xFFFFFFFF);
|
||||||
|
header.numberOfSections = ByteUtils.getUnsingedIntFromBytes(data, 8) & 0xFFFFFFFF;
|
||||||
|
header.hashDisabled = ByteUtils.getByteFromBytes(data, 12);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.put(MAGIC.getBytes());
|
||||||
|
buffer.putInt(4, (int) blockSize.getBlockSize());
|
||||||
|
buffer.putInt(8, (int) numberOfSections);
|
||||||
|
buffer.put(12, (byte) hashDisabled);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
|
public class DirectoryEntry extends NodeEntry {
|
||||||
|
@Getter @Setter protected int parentEntryNumber;
|
||||||
|
|
||||||
|
@Getter @Setter protected int lastEntryNumber;
|
||||||
|
|
||||||
|
@Getter private final List<NodeEntry> children = new ArrayList<>();
|
||||||
|
|
||||||
|
static DirectoryEntry parseData(byte[] data, int offset, NodeEntryParam param, SectionEntries sectionEntries, StringTable stringTable) {
|
||||||
|
|
||||||
|
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||||
|
directoryEntry.entryNumber = param.entryNumber;
|
||||||
|
directoryEntry.parent = param.parent;
|
||||||
|
directoryEntry.entryType = param.type;
|
||||||
|
directoryEntry.nameString = stringTable.getStringEntry(param.uint24);
|
||||||
|
|
||||||
|
directoryEntry.parentEntryNumber = (int) (ByteUtils.getUnsingedIntFromBytes(data, offset + 4) & 0xFFFFFFFF);
|
||||||
|
directoryEntry.lastEntryNumber = (int) (ByteUtils.getUnsingedIntFromBytes(data, offset + 8) & 0xFFFFFFFF);
|
||||||
|
|
||||||
|
directoryEntry.permission = param.permission;
|
||||||
|
|
||||||
|
if (param.sectionNumber > sectionEntries.size()) {
|
||||||
|
throw new IllegalArgumentException("FST_M_Error_Broken_NodeEntry");
|
||||||
|
}
|
||||||
|
directoryEntry.sectionEntry = sectionEntries.get(param.sectionNumber);
|
||||||
|
|
||||||
|
return directoryEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printRecursive(PrintStream out, int space) {
|
||||||
|
super.printRecursive(out, space);
|
||||||
|
for (val child : children) {
|
||||||
|
child.printRecursive(out, space + 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printFullPathRecursive(PrintStream out) {
|
||||||
|
super.printFullPathRecursive(out);
|
||||||
|
for (val child : children) {
|
||||||
|
child.printFullPathRecursive(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printToStringRecursive(PrintStream out) {
|
||||||
|
super.printToStringRecursive(out);
|
||||||
|
for (val child : children) {
|
||||||
|
child.printToStringRecursive(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChild(NodeEntry entry) {
|
||||||
|
if (entry == null) {
|
||||||
|
throw new IllegalArgumentException("entry was empty");
|
||||||
|
}
|
||||||
|
children.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileEntry> getFileChildren() {
|
||||||
|
return getChildren().stream().filter(e -> e.isFile()).map(e -> (FileEntry) e).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DirectoryEntry> getDirChildren() {
|
||||||
|
return getChildren().stream().filter(e -> e.isDirectory()).map(e -> (DirectoryEntry) e).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<NodeEntry> stream() {
|
||||||
|
return Stream.concat(Stream.of(this), getChildren().stream().flatMap(e -> e.stream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DirectoryEntry [entryNumber=" + entryNumber + ", entryType=" + entryType + ", permission=" + permission + ", nameString=" + nameString + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(NodeEntry.LENGTH);
|
||||||
|
byte type = (byte) (entryType.getAsValue() & 0xFF);
|
||||||
|
int typeAndName = (nameString.getAddress() & 0x00FFFFFF) | (type << 24);
|
||||||
|
buffer.putInt(0, typeAndName);
|
||||||
|
|
||||||
|
buffer.putInt(4, parentEntryNumber);
|
||||||
|
buffer.putInt(8, lastEntryNumber);
|
||||||
|
|
||||||
|
buffer.putShort(12, (short) permission.getValue());
|
||||||
|
buffer.putShort(14, (short) sectionEntry.getSectionNumber());
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class EntryType {
|
||||||
|
private final Set<EntryType.EEntryType> entryTypes = new TreeSet<>();
|
||||||
|
|
||||||
|
public EntryType(EEntryType type) {
|
||||||
|
this.entryTypes.add(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType(EEntryType... types) {
|
||||||
|
Collections.addAll(entryTypes, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "EntryType [entryTypes=" + entryTypes + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType(int type) {
|
||||||
|
for (EEntryType v : EEntryType.values()) {
|
||||||
|
if ((v.getValue() & type) == v.getValue()) {
|
||||||
|
entryTypes.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryTypes.contains(EEntryType.Directory)) {
|
||||||
|
entryTypes.remove(EEntryType.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean has(EEntryType val) {
|
||||||
|
return entryTypes.contains(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum EEntryType {
|
||||||
|
File(0), Directory(1), Link(0x80);
|
||||||
|
|
||||||
|
@Getter private int value;
|
||||||
|
|
||||||
|
EEntryType(int val) {
|
||||||
|
this.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EEntryType findByValue(int val) {
|
||||||
|
for (EEntryType v : values()) {
|
||||||
|
if (v.getValue() == val) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAsValue() {
|
||||||
|
int val = 0;
|
||||||
|
|
||||||
|
for (EEntryType e : entryTypes) {
|
||||||
|
val |= e.getValue();
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionAddress;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionBlockSize;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class FileEntry extends NodeEntry {
|
||||||
|
protected SectionAddress address = SectionAddress.empty();
|
||||||
|
@Getter protected long size;
|
||||||
|
|
||||||
|
public static NodeEntry parseData(byte[] data, int offset, NodeEntryParam param, SectionEntries sectionEntries, StringTable stringTable,
|
||||||
|
SectionBlockSize blockSize) {
|
||||||
|
FileEntry entry = new FileEntry();
|
||||||
|
|
||||||
|
entry.entryNumber = param.entryNumber;
|
||||||
|
entry.parent = param.parent;
|
||||||
|
entry.entryType = param.type;
|
||||||
|
entry.nameString = stringTable.getStringEntry(param.uint24);
|
||||||
|
|
||||||
|
entry.address = new SectionAddress(blockSize, ByteUtils.getUnsingedIntFromBytes(data, offset + 4) & 0xFFFFFFFF);
|
||||||
|
entry.size = ByteUtils.getUnsingedIntFromBytes(data, offset + 8) & 0xFFFFFFFF;
|
||||||
|
|
||||||
|
entry.permission = param.permission;
|
||||||
|
entry.sectionEntry = sectionEntries.get(param.sectionNumber);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOffset() {
|
||||||
|
return address.getAddressInBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "FileEntry [address=" + String.format("0x%08X", address.getAddressInBytes()) + ", size=" + String.format("0x%08X", size) + ", entryNumber="
|
||||||
|
+ entryNumber + ", entryType=" + entryType + ", permission=" + permission + ", nameString=" + nameString + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(NodeEntry.LENGTH);
|
||||||
|
byte type = (byte) (entryType.getAsValue() & 0xFF);
|
||||||
|
int typeAndName = (nameString.getAddress() & 0x00FFFFFF) | (type << 24);
|
||||||
|
buffer.putInt(0, typeAndName);
|
||||||
|
|
||||||
|
buffer.putInt(4, (int) address.getValue());
|
||||||
|
buffer.putInt(8, (int) size);
|
||||||
|
|
||||||
|
buffer.putShort(12, (short) permission.getValue());
|
||||||
|
buffer.putShort(14, (short) sectionEntry.getSectionNumber());
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionAddress;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionBlockSize;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
|
public class NodeEntries {
|
||||||
|
|
||||||
|
@Getter private final RootEntry rootEntry;
|
||||||
|
|
||||||
|
public NodeEntries(RootEntry rootEntry) {
|
||||||
|
this.rootEntry = rootEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NodeEntry DeserializeImpl(byte[] data, int offset, DirectoryEntry parent, int entryNumber, SectionEntries sectionEntries,
|
||||||
|
StringTable stringTable, SectionBlockSize blockSize) {
|
||||||
|
NodeEntry nodeEntry = NodeEntry.AutoDeserialize(data, offset, parent, entryNumber, sectionEntries, stringTable, blockSize);
|
||||||
|
if (nodeEntry instanceof DirectoryEntry) {
|
||||||
|
DirectoryEntry dirNode = (DirectoryEntry) nodeEntry;
|
||||||
|
long curEntryNumber = dirNode.getEntryNumber() + 1;
|
||||||
|
while (curEntryNumber < dirNode.getLastEntryNumber()) {
|
||||||
|
NodeEntry entry = NodeEntries.DeserializeImpl(data, offset + ((int) curEntryNumber - (int) dirNode.getEntryNumber()) * NodeEntry.LENGTH,
|
||||||
|
dirNode, (int) curEntryNumber, sectionEntries, stringTable, blockSize);
|
||||||
|
dirNode.addChild(entry);
|
||||||
|
if (entry instanceof DirectoryEntry) {
|
||||||
|
curEntryNumber = ((DirectoryEntry) entry).getLastEntryNumber();
|
||||||
|
} else {
|
||||||
|
curEntryNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodeEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NodeEntries parseData(byte[] data, int offset, SectionEntries sectionEntries, StringTable stringTable, SectionBlockSize blockSize) {
|
||||||
|
NodeEntry rootEntry = NodeEntries.DeserializeImpl(data, offset, (DirectoryEntry) null, 0, sectionEntries, stringTable, blockSize);
|
||||||
|
if (rootEntry instanceof RootEntry) {
|
||||||
|
return new NodeEntries((RootEntry) rootEntry);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("FST_NOT_ROOT_ENTRY");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NodeEntry createFromFolder(File codeFolder, SectionBlockSize blockSize, StringTable stringtable, SectionEntry section,
|
||||||
|
Permission permission) {
|
||||||
|
|
||||||
|
NodeEntry cur = createFromFile(codeFolder, null, stringtable, section, permission);
|
||||||
|
|
||||||
|
val tmpList = cur.stream().filter(f -> f.isFile()).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// calcuate offsets
|
||||||
|
SectionAddress curAddress = new SectionAddress(blockSize, 0L);
|
||||||
|
for (val e : tmpList) {
|
||||||
|
((FileEntry) e).address = new SectionAddress(curAddress.getBlockSize(), curAddress.getValue());
|
||||||
|
val size = ((FileEntry) e).getSize();
|
||||||
|
int alignedSize = (int) Utils.align(size, (int) blockSize.getBlockSize());
|
||||||
|
int fstBlockSize = (int) blockSize.getBlockSize();
|
||||||
|
int offsetDiffInBlocks = alignedSize / fstBlockSize;
|
||||||
|
long newOffset = alignedSize % fstBlockSize == 0 ? offsetDiffInBlocks : offsetDiffInBlocks + 1;
|
||||||
|
curAddress = new SectionAddress(blockSize, newOffset + curAddress.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NodeEntry createFromFile(File inputFile, DirectoryEntry parent, StringTable stringtable, SectionEntry section, Permission permission) {
|
||||||
|
|
||||||
|
StringEntry name = stringtable.getEntry(inputFile.getName()).orElseThrow(() -> new IllegalArgumentException("Failed to find String"));
|
||||||
|
|
||||||
|
NodeEntry result = null;
|
||||||
|
if (inputFile.isDirectory()) {
|
||||||
|
DirectoryEntry curDir = new DirectoryEntry();
|
||||||
|
for (File curFile : inputFile.listFiles()) {
|
||||||
|
NodeEntry child = createFromFile(curFile, curDir, stringtable, section, permission);
|
||||||
|
curDir.addChild(child);
|
||||||
|
}
|
||||||
|
curDir.setEntryType(new EntryType(EEntryType.Directory));
|
||||||
|
result = curDir;
|
||||||
|
} else {
|
||||||
|
result = new RealFileEntry(inputFile);
|
||||||
|
result.setEntryType(new EntryType(EEntryType.File));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setParent(parent);
|
||||||
|
result.setPermission(permission);
|
||||||
|
result.setNameString(name);
|
||||||
|
result.setSectionEntry(section);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSizeInBytes() {
|
||||||
|
return NodeEntry.LENGTH * rootEntry.getLastEntryNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) getSizeInBytes());
|
||||||
|
for (NodeEntry e : rootEntry.stream().collect(Collectors.toList())) {
|
||||||
|
buffer.put(e.getAsBytes());
|
||||||
|
}
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<NodeEntry> streamBy(SectionEntry e) {
|
||||||
|
return rootEntry.stream().filter(f -> f.getSectionEntry().equals(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NodeEntry> getBy(SectionEntry e) {
|
||||||
|
return rootEntry.stream().filter(f -> f.getSectionEntry().equals(e)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SectionBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public abstract class NodeEntry {
|
||||||
|
public static final int LENGTH = 16;
|
||||||
|
|
||||||
|
protected int entryNumber;
|
||||||
|
protected EntryType entryType;
|
||||||
|
protected Permission permission;
|
||||||
|
protected StringEntry nameString;
|
||||||
|
protected SectionEntry sectionEntry;
|
||||||
|
protected DirectoryEntry parent;
|
||||||
|
|
||||||
|
public static NodeEntry AutoDeserialize(byte[] data, int offset, DirectoryEntry parent, int entryNumber, SectionEntries sectionEntries,
|
||||||
|
StringTable stringTable, SectionBlockSize blockSize) {
|
||||||
|
|
||||||
|
byte[] curEntryData = data;
|
||||||
|
int curOffset = (int) offset;
|
||||||
|
if (offset > 0) {
|
||||||
|
curEntryData = Arrays.copyOfRange(curEntryData, curOffset, curOffset + NodeEntry.LENGTH);
|
||||||
|
curOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeEntryParam param = new NodeEntryParam();
|
||||||
|
param.permission = new Permission((int) (ByteUtils.getShortFromBytes(data, offset + 12) & 0xFFFF));
|
||||||
|
param.sectionNumber = (int) (ByteUtils.getShortFromBytes(data, offset + 14) & 0xFFFF);
|
||||||
|
param.entryNumber = entryNumber;
|
||||||
|
param.parent = parent;
|
||||||
|
param.type = new EntryType(ByteUtils.getByteFromBytes(curEntryData, curOffset));
|
||||||
|
|
||||||
|
byte[] entryData = Arrays.copyOfRange(curEntryData, curOffset, curOffset + 4);
|
||||||
|
entryData[0] = 0;
|
||||||
|
param.uint24 = ByteUtils.getIntFromBytes(entryData, 0);
|
||||||
|
|
||||||
|
if (param.type.has(EEntryType.Directory) && param.uint24 == 0) { // Root
|
||||||
|
return (NodeEntry) RootEntry.parseData(curEntryData, 0, param, sectionEntries, stringTable);
|
||||||
|
} else if (param.type.has(EEntryType.Directory)) {
|
||||||
|
return (NodeEntry) DirectoryEntry.parseData(curEntryData, 0, param, sectionEntries, stringTable);
|
||||||
|
} else if (param.type.has(EEntryType.File)) {
|
||||||
|
return (NodeEntry) FileEntry.parseData(curEntryData, 0, param, sectionEntries, stringTable, blockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("FST_UNKNOWN_NODE_TYPE");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return nameString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected StringBuilder getFullPathInternal() {
|
||||||
|
if (parent != null) {
|
||||||
|
return parent.getFullPathInternal().append('/').append(getName());
|
||||||
|
}
|
||||||
|
return new StringBuilder(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullPath() {
|
||||||
|
return getFullPathInternal().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
if (parent != null) {
|
||||||
|
return parent.getFullPath();
|
||||||
|
}
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printRecursive(int space) {
|
||||||
|
printRecursive(System.out, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printRecursive(PrintStream out, int space) {
|
||||||
|
for (int i = 0; i < space; i++) {
|
||||||
|
out.print(" ");
|
||||||
|
}
|
||||||
|
out.print(getName());
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printPathRecursive() {
|
||||||
|
printFullPathRecursive(System.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printFullPathRecursive(PrintStream out) {
|
||||||
|
out.println(getFullPath() +" " + getSectionEntry().getSectionNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printToStringRecursive() {
|
||||||
|
printToStringRecursive(System.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printToStringRecursive(PrintStream out) {
|
||||||
|
out.println(toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return entryType.has(EEntryType.Directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFile() {
|
||||||
|
return entryType.has(EEntryType.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLink() {
|
||||||
|
return entryType.has(EEntryType.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "NodeEntry [entryNumber=" + entryNumber + ", entryType=" + entryType + ", permission=" + permission + ", nameString=" + nameString + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
protected static class NodeEntryParam {
|
||||||
|
public DirectoryEntry parent;
|
||||||
|
public int entryNumber;
|
||||||
|
public int sectionNumber;
|
||||||
|
public Permission permission;
|
||||||
|
protected EntryType type;
|
||||||
|
protected int uint24;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<NodeEntry> stream() {
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract byte[] getAsBytes();
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Permission {
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
public Permission(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class RealFileEntry extends FileEntry {
|
||||||
|
@Getter private final File fd;
|
||||||
|
|
||||||
|
public RealFileEntry(File inputFile) {
|
||||||
|
super();
|
||||||
|
this.fd = inputFile;
|
||||||
|
this.size = inputFile.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntries;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
|
||||||
|
public class RootEntry extends DirectoryEntry {
|
||||||
|
public RootEntry(DirectoryEntry input) {
|
||||||
|
if (!input.entryType.has(EEntryType.Directory) || input.entryNumber != 0) {
|
||||||
|
throw new IllegalArgumentException("Input is no root entry.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entryNumber = input.entryNumber;
|
||||||
|
this.parent = input.parent;
|
||||||
|
this.nameString = input.nameString;
|
||||||
|
this.entryType = input.entryType;
|
||||||
|
|
||||||
|
this.parentEntryNumber = input.parentEntryNumber;
|
||||||
|
this.lastEntryNumber = input.lastEntryNumber;
|
||||||
|
this.permission = input.permission;
|
||||||
|
this.sectionEntry = input.sectionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parseLastEntryNumber(byte[] data, long offset) {
|
||||||
|
return (int) (ByteUtils.getUnsingedIntFromBytes(data, (int) (offset + 8)) & 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RootEntry parseData(byte[] data, int offset, NodeEntryParam param, SectionEntries sectionEntries, StringTable stringTable) {
|
||||||
|
return new RootEntry(DirectoryEntry.parseData(data, offset, param, sectionEntries, stringTable));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.nodeentry;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringEntry;
|
||||||
|
|
||||||
|
public class VirtualFileEntry extends FileEntry {
|
||||||
|
private long offset;
|
||||||
|
|
||||||
|
public static FileEntry create(DirectoryEntry parent, long offset, long size, StringEntry name, Permission permission, SectionEntry section) {
|
||||||
|
VirtualFileEntry entry = new VirtualFileEntry();
|
||||||
|
|
||||||
|
entry.entryNumber = 0;
|
||||||
|
entry.parent = parent;
|
||||||
|
entry.entryType = new EntryType(EEntryType.File);
|
||||||
|
entry.nameString = name;
|
||||||
|
|
||||||
|
entry.offset = offset;
|
||||||
|
entry.size = size;
|
||||||
|
entry.permission = permission;
|
||||||
|
entry.sectionEntry = section;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getOffset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.sectionentry;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.VolumeBlockSize;
|
||||||
|
|
||||||
|
public class SectionEntries extends LinkedList<SectionEntry> {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public static SectionEntries parseData(byte[] data, long offset, long numberOfSections, VolumeBlockSize blockSize) {
|
||||||
|
SectionEntries sectionEntries = new SectionEntries();
|
||||||
|
for (int i = 0; i < numberOfSections; i++) {
|
||||||
|
sectionEntries.add(SectionEntry.parseData(data, offset + (i * 32), "Section: " + i, i, blockSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sectionEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSizeInBytes() {
|
||||||
|
return size() * SectionEntry.LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) getSizeInBytes());
|
||||||
|
for (SectionEntry entry : this) {
|
||||||
|
buffer.put(entry.getAsBytes());
|
||||||
|
}
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.sectionentry;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SizeInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.VolumeBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SectionEntry {
|
||||||
|
public static final int LENGTH = 32;
|
||||||
|
private final int sectionNumber;
|
||||||
|
private final String name;
|
||||||
|
private AddressInVolumeBlocks address;
|
||||||
|
private SizeInVolumeBlocks size = new SizeInVolumeBlocks(new VolumeBlockSize(0), (long) 0);
|
||||||
|
private long ownerID;
|
||||||
|
private long groupID;
|
||||||
|
private short hashMode;
|
||||||
|
|
||||||
|
public static SectionEntry parseData(byte[] data, long offset, String sectionName, int sectionNumber, VolumeBlockSize blockSize) {
|
||||||
|
SectionEntry sectionEntry = new SectionEntry(sectionNumber, sectionName);
|
||||||
|
|
||||||
|
sectionEntry.address = new AddressInVolumeBlocks(blockSize, ByteUtils.getUnsingedIntFromBytes(data, (int) offset) & 0xFFFFFFFF);
|
||||||
|
sectionEntry.size = new SizeInVolumeBlocks(blockSize, ByteUtils.getUnsingedIntFromBytes(data, (int) offset + 4) & 0xFFFFFFFF);
|
||||||
|
sectionEntry.ownerID = ByteUtils.getLongFromBytes(data, (int) offset + 8);
|
||||||
|
sectionEntry.groupID = ByteUtils.getUnsingedIntFromBytes(data, (int) offset + 16) & 0xFFFFFFFF;
|
||||||
|
sectionEntry.hashMode = ByteUtils.getByteFromBytes(data, (int) offset + 20);
|
||||||
|
|
||||||
|
return sectionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.putInt(0, (int) address.getValue());
|
||||||
|
buffer.putInt(4, (int) size.getValue());
|
||||||
|
buffer.putLong(8, ownerID);
|
||||||
|
buffer.putInt(16, (int) groupID);
|
||||||
|
buffer.put(20, (byte) hashMode);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SectionEntry [sectionNumber=" + sectionNumber + ", name=" + name + ", address=" + (address.getAddressInBytes() / 1024.0) + " KiB , size="
|
||||||
|
+ (size.getSizeInBytes() / 1024.0) + " KiB , ownerID=" + String.format("%016X", ownerID) + ", groupID=" + String.format("%04X", groupID)
|
||||||
|
+ ", hashMode=" + hashMode + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.stringtable;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class StringEntry {
|
||||||
|
private final StringTable stringTable;
|
||||||
|
private final int address;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return stringTable.getByAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package de.mas.wiiu.jnus.entities.FST.stringtable;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class StringTable {
|
||||||
|
private final Map<Integer, StringEntry> stringEntries = new HashMap<>();
|
||||||
|
private final Map<Integer, String> strings = new HashMap<>();
|
||||||
|
|
||||||
|
public StringTable(String... strings) {
|
||||||
|
this(Arrays.asList(strings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringTable(Collection<String> strings) {
|
||||||
|
int curLength = 0;
|
||||||
|
this.strings.put(curLength, "");
|
||||||
|
this.stringEntries.put(curLength, new StringEntry(this, curLength));
|
||||||
|
curLength = 1;
|
||||||
|
for (String s : strings) {
|
||||||
|
this.stringEntries.put(curLength, new StringEntry(this, curLength));
|
||||||
|
this.strings.put(curLength, s);
|
||||||
|
curLength += s.length() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringTable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StringTable parseData(byte[] data, long offset, long stringsCount) {
|
||||||
|
StringTable stringTable = new StringTable();
|
||||||
|
long curOffset = offset;
|
||||||
|
int i;
|
||||||
|
for (i = 0; curOffset < data.length && i < stringsCount; ++curOffset) {
|
||||||
|
if (data[(int) curOffset] == (byte) 0) {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < stringsCount) {
|
||||||
|
throw new IllegalArgumentException("StringTable is broken");
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] strArray = new String(Arrays.copyOfRange(data, (int) offset, (int) (curOffset))).split("\0");
|
||||||
|
int curLength = 0;
|
||||||
|
for (i = 0; i < strArray.length; i++) {
|
||||||
|
stringTable.stringEntries.put(curLength, new StringEntry(stringTable, curLength));
|
||||||
|
stringTable.strings.put(curLength, strArray[i]);
|
||||||
|
curLength += strArray[i].length() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getByAddress(int address) {
|
||||||
|
return strings.get(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringEntry getStringEntry(int address) {
|
||||||
|
StringEntry entry = stringEntries.get(address);
|
||||||
|
if (entry == null) {
|
||||||
|
throw new IllegalArgumentException("Failed to find string entry for address: " + address);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
int capacity = 1; // root entry
|
||||||
|
for (String s : this.strings.values()) {
|
||||||
|
capacity += s.length() + 1;
|
||||||
|
}
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<StringEntry> getEntry(String str) {
|
||||||
|
return stringEntries.entrySet().stream().filter(e -> e.getValue().toString().equals(str)).map(e -> e.getValue()).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) getSize());
|
||||||
|
for (Entry<Integer, String> s : this.strings.entrySet()) {
|
||||||
|
buffer.position(s.getKey());
|
||||||
|
buffer.put(s.getValue().getBytes());
|
||||||
|
}
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,7 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
@ -182,7 +182,7 @@ public class DefaultNUSDataProcessor implements NUSDataProcessor {
|
|||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
InputStream in = readDecryptedContentAsStream(c, offset, size);
|
InputStream in = readDecryptedContentAsStream(c, offset, Utils.align(size, 16));
|
||||||
|
|
||||||
byte[] hash = null;
|
byte[] hash = null;
|
||||||
if (forceCheckHash) {
|
if (forceCheckHash) {
|
||||||
|
@ -21,8 +21,16 @@ import java.io.OutputStream;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.NUSTitle;
|
import de.mas.wiiu.jnus.NUSTitle;
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.EntryType.EEntryType;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.Permission;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.VirtualFileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.stringtable.StringTable;
|
||||||
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
import de.mas.wiiu.jnus.interfaces.HasNUSTitle;
|
import de.mas.wiiu.jnus.interfaces.HasNUSTitle;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||||
@ -34,7 +42,7 @@ import lombok.extern.java.Log;
|
|||||||
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
||||||
private final NUSDataProcessor dataProcessor;
|
private final NUSDataProcessor dataProcessor;
|
||||||
private final NUSTitle title;
|
private final NUSTitle title;
|
||||||
private final FSTEntry rootEntry;
|
private final RootEntry rootEntry;
|
||||||
@Getter @Setter private String name;
|
@Getter @Setter private String name;
|
||||||
|
|
||||||
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
|
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
|
||||||
@ -43,12 +51,24 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
|||||||
this.name = String.format("%016X", title.getTMD().getTitleID());
|
this.name = String.format("%016X", title.getTMD().getTitleID());
|
||||||
|
|
||||||
if (title.getFST().isPresent()) {
|
if (title.getFST().isPresent()) {
|
||||||
rootEntry = title.getFST().get().getRoot();
|
rootEntry = title.getFST().get().getRootEntry();
|
||||||
} else if (title.getTMD().getContentCount() == 1) {
|
} else if (title.getTMD().getContentCount() == 1) {
|
||||||
// If the tmd has only one content file, it has not FST. We have to create our own FST.
|
// If the tmd has only one content file, it has not FST. We have to create our own FST.
|
||||||
Content c = title.getTMD().getAllContents().values().stream().collect(Collectors.toList()).get(0);
|
Content c = title.getTMD().getAllContents().values().stream().collect(Collectors.toList()).get(0);
|
||||||
FSTEntry root = FSTEntry.getRootFSTEntry();
|
|
||||||
root.addChildren(FSTEntry.createFSTEntry(root, "data.bin", c));
|
StringTable stringtable = new StringTable("", "data.bin");
|
||||||
|
SectionEntry dummySection = new SectionEntry(0, "dummy");
|
||||||
|
DirectoryEntry dir = new DirectoryEntry();
|
||||||
|
dir.setEntryType(new EntryType(EEntryType.Directory));
|
||||||
|
dir.setParent(null);
|
||||||
|
dir.setEntryNumber(0);
|
||||||
|
dir.setNameString(stringtable.getEntry("").get());
|
||||||
|
dir.setPermission(new Permission(0));
|
||||||
|
dir.setSectionEntry(dummySection);
|
||||||
|
|
||||||
|
RootEntry root = new RootEntry(dir);
|
||||||
|
|
||||||
|
root.addChild(VirtualFileEntry.create(root, 0, c.getDecryptedFileSize(), stringtable.getEntry("data.bin").get(), new Permission(0), dummySection));
|
||||||
rootEntry = root;
|
rootEntry = root;
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("No FST root entry was found");
|
throw new IOException("No FST root entry was found");
|
||||||
@ -56,23 +76,20 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FSTEntry getRoot() {
|
public RootEntry getRoot() {
|
||||||
return rootEntry;
|
return rootEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException {
|
||||||
if (entry.isNotInPackage()) {
|
if (entry.isLink()) {
|
||||||
if (entry.isNotInPackage()) {
|
|
||||||
log.info("Decryption not possible because the FSTEntry is not in this package");
|
log.info("Decryption not possible because the FSTEntry is not in this package");
|
||||||
}
|
|
||||||
out.close();
|
out.close();
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
Content c = title.getTMD().getContentByIndex(entry.getSectionEntry().getSectionNumber());
|
||||||
|
|
||||||
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
|
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getOffset(), size, size == entry.getSize());
|
||||||
|
|
||||||
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -19,52 +19,53 @@ package de.mas.wiiu.jnus.implementations;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDDataPartition;
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUDataPartition;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
|
||||||
|
|
||||||
public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
|
public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
|
||||||
private final WUDDataPartition partition;
|
private final WiiUDataPartition partition;
|
||||||
private final WUDDiscReader discReader;
|
private final WUDDiscReader discReader;
|
||||||
private final byte[] titleKey;
|
private final Optional<byte[]> discKey;
|
||||||
|
|
||||||
public FSTDataProviderWUDDataPartition(WUDDataPartition partition, WUDDiscReader discReader, byte[] titleKey) {
|
public FSTDataProviderWUDDataPartition(WiiUDataPartition partition, WUDDiscReader discReader, Optional<byte[]> discKey) {
|
||||||
this.partition = partition;
|
this.partition = partition;
|
||||||
this.discReader = discReader;
|
this.discReader = discReader;
|
||||||
this.titleKey = titleKey;
|
this.discKey = discKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return partition.getPartitionName();
|
return partition.getVolumeID();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FSTEntry getRoot() {
|
public RootEntry getRoot() {
|
||||||
return partition.getFST().getRoot();
|
return partition.getFst().getRootEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException {
|
||||||
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
|
SectionEntry info = entry.getSectionEntry();
|
||||||
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
|
if (!discKey.isPresent()) {
|
||||||
if (titleKey == null) {
|
return discReader.readEncryptedToStream(out,
|
||||||
return discReader.readEncryptedToStream(out, partition.getPartitionOffset() + info.getOffset() + entry.getFileOffset() + offset, size);
|
partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes() + entry.getOffset() + offset, size);
|
||||||
}
|
}
|
||||||
return discReader.readDecryptedToOutputStream(out, partition.getPartitionOffset() + info.getOffset(), entry.getFileOffset() + offset, size, titleKey,
|
return discReader.readDecryptedToOutputStream(out, partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes(),
|
||||||
null, false);
|
entry.getOffset() + offset, size, discKey.get(), null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
|
public InputStream readFileAsStream(FileEntry entry, long offset, long size) throws IOException {
|
||||||
if (titleKey == null) {
|
if (!discKey.isPresent()) {
|
||||||
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
|
SectionEntry info = entry.getSectionEntry();
|
||||||
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
|
return discReader.readEncryptedToStream(
|
||||||
return discReader.readEncryptedToStream(partition.getPartitionOffset() + info.getOffset() + entry.getFileOffset() + offset, size);
|
partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes() + entry.getOffset() + offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FSTDataProvider.super.readFileAsStream(entry, offset, size);
|
return FSTDataProvider.super.readFileAsStream(entry, offset, size);
|
||||||
|
@ -7,7 +7,8 @@ import java.io.OutputStream;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.wumad.WumadDataPartition;
|
import de.mas.wiiu.jnus.implementations.wud.wumad.WumadDataPartition;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
@ -27,24 +28,23 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FSTEntry getRoot() {
|
public RootEntry getRoot() {
|
||||||
return dataPartition.getFST().getRoot();
|
return dataPartition.getFST().getRootEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException {
|
||||||
return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
|
return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
|
public InputStream readFileAsStream(FileEntry entry, long offset, long size) throws IOException {
|
||||||
ZipEntry zipEntry = zipFile.stream()
|
ZipEntry zipEntry = zipFile.stream().filter(
|
||||||
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex())))
|
e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getSectionEntry().getSectionNumber())))
|
||||||
.findFirst()
|
.findFirst().orElseThrow(() -> new FileNotFoundException());
|
||||||
.orElseThrow(() -> new FileNotFoundException());
|
|
||||||
|
|
||||||
InputStream in = zipFile.getInputStream(zipEntry);
|
InputStream in = zipFile.getInputStream(zipEntry);
|
||||||
StreamUtils.skipExactly(in, offset + entry.getFileOffset());
|
StreamUtils.skipExactly(in, offset + entry.getOffset());
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,10 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
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.FST.nodeentry.DirectoryEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
@ -33,9 +35,9 @@ import de.mas.wiiu.jnus.utils.FSTUtils;
|
|||||||
|
|
||||||
public class NUSDataProviderFST implements NUSDataProvider {
|
public class NUSDataProviderFST implements NUSDataProvider {
|
||||||
private final FSTDataProvider fstDataProvider;
|
private final FSTDataProvider fstDataProvider;
|
||||||
private final FSTEntry base;
|
private final DirectoryEntry base;
|
||||||
|
|
||||||
public NUSDataProviderFST(FSTDataProvider fstDataProvider, FSTEntry base) {
|
public NUSDataProviderFST(FSTDataProvider fstDataProvider, DirectoryEntry base) {
|
||||||
this.base = base;
|
this.base = base;
|
||||||
this.fstDataProvider = fstDataProvider;
|
this.fstDataProvider = fstDataProvider;
|
||||||
}
|
}
|
||||||
@ -47,9 +49,12 @@ public class NUSDataProviderFST implements NUSDataProvider {
|
|||||||
@Override
|
@Override
|
||||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
||||||
String filename = content.getFilename();
|
String filename = content.getFilename();
|
||||||
Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
|
Optional<NodeEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
|
||||||
FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));
|
NodeEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));
|
||||||
return fstDataProvider.readFileAsStream(contentFile, offset, size);
|
if (!contentFile.isFile() || contentFile.isLink()) {
|
||||||
|
throw new IOException("Requested file is not a file or is a link");
|
||||||
|
}
|
||||||
|
return fstDataProvider.readFileAsStream((FileEntry) contentFile, offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Integer, Optional<byte[]>> h3Hashes = new HashMap<>();
|
Map<Integer, Optional<byte[]>> h3Hashes = new HashMap<>();
|
||||||
@ -64,12 +69,14 @@ public class NUSDataProviderFST implements NUSDataProvider {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<byte[]> readFileByFilename(FSTEntry base, String filename) throws IOException {
|
private Optional<byte[]> readFileByFilename(DirectoryEntry base, String filename) throws IOException {
|
||||||
Optional<FSTEntry> entryOpt = FSTUtils.getChildOfDirectory(base, filename);
|
Optional<NodeEntry> entryOpt = FSTUtils.getChildOfDirectory(base, filename);
|
||||||
if (entryOpt.isPresent()) {
|
if (entryOpt.isPresent()) {
|
||||||
|
NodeEntry entry = entryOpt.get();
|
||||||
FSTEntry entry = entryOpt.get();
|
if (!entry.isFile() || entry.isLink()) {
|
||||||
return Optional.of(fstDataProvider.readFile(entry));
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(fstDataProvider.readFile((FileEntry) entry));
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import java.nio.file.Files;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.FileUtils;
|
import de.mas.wiiu.jnus.utils.FileUtils;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
|
@ -25,7 +25,7 @@ import java.nio.file.Files;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -23,7 +23,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.interfaces.Parallelizable;
|
import de.mas.wiiu.jnus.interfaces.Parallelizable;
|
||||||
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
|
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
|
||||||
@ -32,15 +32,16 @@ import lombok.Getter;
|
|||||||
public class NUSDataProviderRemote implements NUSDataProvider, 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;
|
||||||
|
private final NUSDownloadService downloadService;
|
||||||
|
|
||||||
public NUSDataProviderRemote(int version, long titleID) {
|
public NUSDataProviderRemote(int version, long titleID, NUSDownloadService downloadService) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.titleID = titleID;
|
this.titleID = titleID;
|
||||||
|
this.downloadService = downloadService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
||||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
|
||||||
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
|
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,13 +69,11 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
|
|||||||
return resOpt;
|
return resOpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<byte[]> tmdCache = null;
|
Optional<byte[]> tmdCache = Optional.empty();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<byte[]> getRawTMD() throws IOException {
|
public Optional<byte[]> getRawTMD() throws IOException {
|
||||||
if (tmdCache == null) {
|
if (!tmdCache.isPresent()) {
|
||||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
|
||||||
|
|
||||||
long titleID = getTitleID();
|
long titleID = getTitleID();
|
||||||
int version = getVersion();
|
int version = getVersion();
|
||||||
|
|
||||||
@ -88,12 +87,11 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
|
|||||||
return tmdCache;
|
return tmdCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<byte[]> ticketCache = null;
|
Optional<byte[]> ticketCache = Optional.empty();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<byte[]> getRawTicket() throws IOException {
|
public Optional<byte[]> getRawTicket() throws IOException {
|
||||||
if (ticketCache == null) {
|
if (!ticketCache.isPresent()) {
|
||||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
|
||||||
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
||||||
if (res == null || res.length == 0) {
|
if (res == null || res.length == 0) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
@ -18,41 +18,54 @@ package de.mas.wiiu.jnus.implementations;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUGMPartition;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SizeInVolumeBlocks;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.var;
|
||||||
import lombok.extern.java.Log;
|
import lombok.extern.java.Log;
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class NUSDataProviderWUD implements NUSDataProvider {
|
public class NUSDataProviderWUD implements NUSDataProvider {
|
||||||
@Getter private final WUDGamePartition gamePartition;
|
@Getter private final WiiUGMPartition gamePartition;
|
||||||
@Getter private final WUDDiscReader discReader;
|
@Getter private final WUDDiscReader discReader;
|
||||||
@Getter private FST fst;
|
@Getter private FST fst;
|
||||||
|
|
||||||
public NUSDataProviderWUD(WUDGamePartition gamePartition, WUDDiscReader discReader) {
|
public NUSDataProviderWUD(WiiUGMPartition gamePartition, WUDDiscReader discReader) {
|
||||||
this.gamePartition = gamePartition;
|
this.gamePartition = gamePartition;
|
||||||
this.discReader = discReader;
|
this.discReader = discReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFST(FST fst) {
|
public void setFST(FST fst) {
|
||||||
|
// We need to set the correct blocksizes
|
||||||
|
var blockSize = gamePartition.getVolumes().values().iterator().next().getBlockSize();
|
||||||
|
for (SectionEntry e : fst.getSectionEntries()) {
|
||||||
|
e.setAddress(new AddressInVolumeBlocks(blockSize, e.getAddress().getValue()));
|
||||||
|
e.setSize(new SizeInVolumeBlocks(blockSize, e.getSize().getValue()));
|
||||||
|
}
|
||||||
this.fst = fst;
|
this.fst = fst;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getOffsetInWUD(Content content) throws IOException {
|
public long getOffsetInWUD(Content content) throws IOException {
|
||||||
if (content.getIndex() == 0) { // Index 0 is the FST which is at the beginning of the partion;
|
if (content.getIndex() == 0) { // Index 0 is the FST which is at the beginning of the partition;
|
||||||
return getGamePartition().getPartitionOffset();
|
var vh = getGamePartition().getVolumes().values().iterator().next();
|
||||||
|
return getGamePartition().getSectionOffsetOnDefaultPartition() + vh.getFSTAddress().getAddressInBytes();
|
||||||
}
|
}
|
||||||
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(fst, content.getIndex()).orElseThrow(() -> new IOException("Failed to find FSTInfo"));
|
SectionEntry info = FSTUtils.getSectionEntryForIndex(fst, content.getIndex()).orElseThrow(() -> new IOException("Failed to find FSTInfo"));
|
||||||
return getGamePartition().getPartitionOffset() + info.getOffset();
|
return getGamePartition().getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -60,16 +73,22 @@ public class NUSDataProviderWUD implements NUSDataProvider {
|
|||||||
WUDDiscReader discReader = getDiscReader();
|
WUDDiscReader discReader = getDiscReader();
|
||||||
long offset = getOffsetInWUD(content) + fileOffsetBlock;
|
long offset = getOffsetInWUD(content) + fileOffsetBlock;
|
||||||
|
|
||||||
return discReader.readEncryptedToStream(offset, size);
|
return discReader.readEncryptedToStream(offset, Utils.align(size, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
|
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
|
||||||
if (!getGamePartitionHeader().isCalculatedHashes()) {
|
byte[] hash = getGamePartition().getVolumes().values().iterator().next().getH3HashArrayList().get(content.getIndex()).getH3HashArray();
|
||||||
log.info("Calculating h3 hashes");
|
// Checking the hash of the h3 file.
|
||||||
getGamePartitionHeader().calculateHashes(getGamePartition().getTmd().getAllContents());
|
try {
|
||||||
|
if (!Arrays.equals(HashUtil.hashSHA1(hash), content.getSHA2Hash())) {
|
||||||
|
log.warning("h3 incorrect from WUD");
|
||||||
}
|
}
|
||||||
return getGamePartitionHeader().getH3Hash(content);
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.warning(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -87,10 +106,6 @@ public class NUSDataProviderWUD implements NUSDataProvider {
|
|||||||
return Optional.of(getGamePartition().getRawCert());
|
return Optional.of(getGamePartition().getRawCert());
|
||||||
}
|
}
|
||||||
|
|
||||||
public GamePartitionHeader getGamePartitionHeader() {
|
|
||||||
return getGamePartition().getPartitionHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
// We don't need it
|
// We don't need it
|
||||||
|
@ -24,7 +24,7 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipException;
|
import java.util.zip.ZipException;
|
||||||
|
|
||||||
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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyZipFile;
|
import de.mas.wiiu.jnus.implementations.woomy.WoomyZipFile;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
|
@ -19,7 +19,8 @@ package de.mas.wiiu.jnus.implementations;
|
|||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.ParseException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -28,13 +29,14 @@ import java.util.Optional;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.wumad.WumadGamePartition;
|
import de.mas.wiiu.jnus.implementations.wud.wumad.WumadGamePartition;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
|
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
|
import lombok.extern.java.Log;
|
||||||
|
|
||||||
|
@Log
|
||||||
public class NUSDataProviderWumad implements NUSDataProvider {
|
public class NUSDataProviderWumad implements NUSDataProvider {
|
||||||
private final ZipFile wumad;
|
private final ZipFile wumad;
|
||||||
private final WumadGamePartition partition;
|
private final WumadGamePartition partition;
|
||||||
@ -72,15 +74,17 @@ public class NUSDataProviderWumad implements NUSDataProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
|
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
|
||||||
GamePartitionHeader partitionHeader = partition.getPartitionHeader();
|
byte[] hash = partition.getPartitionHeader().getH3HashArrayList().get(content.getIndex()).getH3HashArray();
|
||||||
if (!partitionHeader.isCalculatedHashes()) {
|
// Checking the hash of the h3 file.
|
||||||
try {
|
try {
|
||||||
partitionHeader.calculateHashes(TMD.parseTMD(getRawTMD().get()).getAllContents());
|
if (!Arrays.equals(HashUtil.hashSHA1(hash), content.getSHA2Hash())) {
|
||||||
} catch (ParseException e) {
|
log.warning("h3 incorrect from WUD");
|
||||||
throw new IOException(e);
|
|
||||||
}
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.warning(e.getMessage());
|
||||||
}
|
}
|
||||||
return partitionHeader.getH3Hash(content);
|
|
||||||
|
return Optional.of(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud;
|
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
|
||||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
|
||||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.java.Log;
|
|
||||||
|
|
||||||
@Log
|
|
||||||
public final class GamePartitionHeader {
|
|
||||||
@Getter @Setter private boolean calculatedHashes = false;
|
|
||||||
@Getter private final HashMap<Short, byte[]> h3Hashes = new HashMap<>();
|
|
||||||
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE) private byte[] rawData;
|
|
||||||
|
|
||||||
private GamePartitionHeader() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: real processing. Currently we are ignoring everything except the hashes
|
|
||||||
public static GamePartitionHeader parseHeader(byte[] header) {
|
|
||||||
GamePartitionHeader result = new GamePartitionHeader();
|
|
||||||
result.setRawData(header);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addH3Hashes(short index, byte[] hash) {
|
|
||||||
getH3Hashes().put(index, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<byte[]> getH3Hash(Content content) {
|
|
||||||
if (content == null) {
|
|
||||||
log.info("Can't find h3 hash, given content is null.");
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(getH3Hashes().get(content.getIndex()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void calculateHashes(Map<Integer, Content> allContents) {
|
|
||||||
byte[] header = getRawData();
|
|
||||||
|
|
||||||
// Calculating offset for the hashes
|
|
||||||
int cnt = ByteUtils.getIntFromBytes(header, 0x10);
|
|
||||||
int start_offset = 0x40 + cnt * 0x04;
|
|
||||||
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
// We have to make sure, that the list is ordered by index
|
|
||||||
List<Content> contents = new ArrayList<>(allContents.values());
|
|
||||||
Collections.sort(contents, (o1, o2) -> Short.compare(o1.getIndex(), o2.getIndex()));
|
|
||||||
|
|
||||||
for (Content c : allContents.values()) {
|
|
||||||
if (!c.isHashed() || !c.isEncrypted()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The encrypted content are splitted in 0x10000 chunk. For each 0x1000 chunk we need one entry in the h3
|
|
||||||
int cnt_hashes = (int) (c.getEncryptedFileSize() / 0x10000 / 0x1000) + 1;
|
|
||||||
|
|
||||||
byte[] hash = Arrays.copyOfRange(header, start_offset + offset * 0x14, start_offset + (offset + cnt_hashes) * 0x14);
|
|
||||||
|
|
||||||
// Checking the hash of the h3 file.
|
|
||||||
try {
|
|
||||||
if (!Arrays.equals(HashUtil.hashSHA1(hash), c.getSHA2Hash())) {
|
|
||||||
log.info("h3 incorrect from WUD");
|
|
||||||
}
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
log.warning(e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
addH3Hashes(c.getIndex(), hash);
|
|
||||||
offset += cnt_hashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCalculatedHashes(true);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.header.WiiUDiscHeader;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUDisc {
|
||||||
|
public final static long DISC_SIZE = 25025314816L;
|
||||||
|
private WiiUDiscHeader header = new WiiUDiscHeader();
|
||||||
|
private Optional<WUDDiscReader> reader;
|
||||||
|
private Optional<byte[]> discKey;
|
||||||
|
|
||||||
|
public static WiiUDisc parseData(WUDDiscReader reader, Optional<byte[]> discKey) throws IOException {
|
||||||
|
WiiUDisc disc = new WiiUDisc();
|
||||||
|
disc.setReader(Optional.of(reader));
|
||||||
|
disc.setHeader(WiiUDiscHeader.parseData(reader, discKey));
|
||||||
|
disc.setDiscKey(discKey);
|
||||||
|
return disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() throws IOException {
|
||||||
|
return header.getAsBytes();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUPartitions;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUContentsInformation {
|
||||||
|
|
||||||
|
public static final int LENGTH = 32768;
|
||||||
|
private WiiUDiscContentsHeader discContentHeader = new WiiUDiscContentsHeader();
|
||||||
|
private WiiUPartitions partitions = new WiiUPartitions();
|
||||||
|
|
||||||
|
public static WiiUContentsInformation parseData(WUDDiscReader reader, Optional<byte[]> discKey, long offset) throws IOException {
|
||||||
|
|
||||||
|
WiiUContentsInformation contentsInformation = new WiiUContentsInformation();
|
||||||
|
long curOffset = offset;
|
||||||
|
WiiUDiscContentsHeader discContentHeader = WiiUDiscContentsHeader.parseData(reader, discKey, curOffset);
|
||||||
|
contentsInformation.setDiscContentHeader(discContentHeader);
|
||||||
|
curOffset += WiiUDiscContentsHeader.LENGTH;
|
||||||
|
contentsInformation.setPartitions(
|
||||||
|
WiiUPartitions.parseData(reader, discKey, curOffset, discContentHeader.getNumberOfPartition(), discContentHeader.getBlockSize()));
|
||||||
|
curOffset += WiiUPartitions.LENGTH;
|
||||||
|
|
||||||
|
if (curOffset - offset != LENGTH) {
|
||||||
|
throw new IOException("Length mismatch. " + (curOffset - offset) + " " + LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentsInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.put(discContentHeader.getAsBytes());
|
||||||
|
buffer.put(partitions.getAsBytes());
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.DiscBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUDiscContentsHeader {
|
||||||
|
public static final int LENGTH = 2048;
|
||||||
|
public static final int MAGIC = 0xCCA6E67B;
|
||||||
|
|
||||||
|
private DiscBlockSize blockSize = new DiscBlockSize(32768);
|
||||||
|
private int numberOfPartition = 0;
|
||||||
|
private byte[] tocHash = new byte[20];
|
||||||
|
|
||||||
|
public static WiiUDiscContentsHeader parseData(WUDDiscReader reader, Optional<byte[]> discKey, long offset) throws IOException {
|
||||||
|
byte[] rawBytes = new byte[0];
|
||||||
|
if (!discKey.isPresent()) {
|
||||||
|
rawBytes = reader.readEncryptedToByteArray(offset, 0, LENGTH);
|
||||||
|
} else {
|
||||||
|
rawBytes = reader.readDecryptedToByteArray(offset, 0, LENGTH, discKey.get(), null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawBytes.length != LENGTH) {
|
||||||
|
throw new IOException("Failed to read WiiUDiscContentsHeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(rawBytes);
|
||||||
|
byte[] magicCompare = new byte[4];
|
||||||
|
buffer.get(magicCompare);
|
||||||
|
if (!Arrays.equals(magicCompare, ByteUtils.getBytesFromInt(MAGIC))) {
|
||||||
|
throw new IOException("WiiUDiscContentsHeader MAGIC mismatch.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WiiUDiscContentsHeader contentsHeader = new WiiUDiscContentsHeader();
|
||||||
|
contentsHeader.setBlockSize(new DiscBlockSize(ByteUtils.getUnsingedIntFromBytes(rawBytes, 4) & 0xFFFFFFFF));
|
||||||
|
contentsHeader.setTocHash(Arrays.copyOfRange(rawBytes, 8, 28));
|
||||||
|
contentsHeader.setNumberOfPartition(ByteUtils.getIntFromBytes(rawBytes, 28));
|
||||||
|
return contentsHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "WiiUDiscContentsHeader [blockSize=" + blockSize + ", numberOfPartition=" + numberOfPartition + ", tocHash=" + Utils.ByteArrayToString(tocHash)
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.put(ByteUtils.getBytesFromInt(MAGIC));
|
||||||
|
buffer.putInt((int) blockSize.getBlockSize());
|
||||||
|
buffer.put(tocHash);
|
||||||
|
buffer.putInt(numberOfPartition);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class WiiUDataPartition extends WiiUPartition {
|
||||||
|
|
||||||
|
@Getter private final FST fst;
|
||||||
|
|
||||||
|
public WiiUDataPartition(WiiUPartition partition, FST fst) {
|
||||||
|
this.setFileSystemDescriptor(partition.getFileSystemDescriptor());
|
||||||
|
this.setVolumeID(partition.getVolumeID());
|
||||||
|
this.getVolumes().putAll(partition.getVolumes());
|
||||||
|
this.fst = fst;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class WiiUGMPartition extends WiiUPartition {
|
||||||
|
@Getter private final byte[] rawTicket;
|
||||||
|
@Getter private final byte[] rawTMD;
|
||||||
|
@Getter private final byte[] rawCert;
|
||||||
|
|
||||||
|
public WiiUGMPartition(WiiUPartition partition, byte[] rawTIK, byte[] rawTMD, byte[] rawCert) {
|
||||||
|
this.setFileSystemDescriptor(partition.getFileSystemDescriptor());
|
||||||
|
this.setVolumeID(partition.getVolumeID());
|
||||||
|
this.getVolumes().putAll(partition.getVolumes());
|
||||||
|
this.rawCert = rawCert;
|
||||||
|
this.rawTMD = rawTMD;
|
||||||
|
this.rawTicket = rawTIK;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes.VolumeHeader;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInDiscBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.DiscBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUPartition {
|
||||||
|
public static final int LENGTH = 128;
|
||||||
|
private final Map<AddressInDiscBlocks, VolumeHeader> volumes = new HashMap<>();
|
||||||
|
private String volumeID;
|
||||||
|
private short fileSystemDescriptor;
|
||||||
|
|
||||||
|
public static WiiUPartition parseData(WUDDiscReader reader, Optional<byte[]> discKey, long offset, DiscBlockSize blockSize) throws IOException {
|
||||||
|
byte[] rawBytes = new byte[0];
|
||||||
|
if (!discKey.isPresent()) {
|
||||||
|
rawBytes = Arrays.copyOfRange(reader.readEncryptedToByteArray(offset, 0, LENGTH), 0, LENGTH);
|
||||||
|
} else {
|
||||||
|
// Hacky solution for not knowing the correct IV.
|
||||||
|
rawBytes = Arrays.copyOfRange(reader.readDecryptedToByteArray(offset - 0x10, 0, LENGTH, discKey.get(), null, true), 0x10, LENGTH + 0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawBytes.length != LENGTH) {
|
||||||
|
throw new IOException("Failed to read WiiUPartition");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(rawBytes);
|
||||||
|
|
||||||
|
byte[] strRaw = Arrays.copyOfRange(rawBytes, 0, 31);
|
||||||
|
for (int i = 0; i < strRaw.length; i++) {
|
||||||
|
if (strRaw[i] == '\0') {
|
||||||
|
strRaw = Arrays.copyOf(strRaw, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String volumeID = new String(strRaw, Charset.forName("ISO-8859-1"));
|
||||||
|
buffer.position(31);
|
||||||
|
int num = (int) buffer.get();
|
||||||
|
|
||||||
|
WiiUPartition partition = new WiiUPartition();
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
AddressInDiscBlocks discLbaAddress = new AddressInDiscBlocks(blockSize,
|
||||||
|
ByteUtils.getUnsingedIntFromBytes(rawBytes, 32 + (int) (i * 4)) & 0xFFFFFFFF);
|
||||||
|
VolumeHeader vh = VolumeHeader.parseData(reader, discLbaAddress.getAddressInBytes());
|
||||||
|
partition.volumes.put(discLbaAddress, vh);
|
||||||
|
}
|
||||||
|
buffer.position(64);
|
||||||
|
short fileSystemDescriptor = buffer.getShort();
|
||||||
|
|
||||||
|
partition.setVolumeID(volumeID);
|
||||||
|
partition.setFileSystemDescriptor(fileSystemDescriptor);
|
||||||
|
|
||||||
|
return partition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSectionOffsetOnDefaultPartition() throws IOException {
|
||||||
|
if (getVolumes().size() != 1) {
|
||||||
|
throw new IOException("We have more or less than 1 volume header.");
|
||||||
|
}
|
||||||
|
return getVolumes().keySet().iterator().next().getAddressInBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.put(volumeID.getBytes());
|
||||||
|
buffer.position(31);
|
||||||
|
buffer.put((byte) volumes.size());
|
||||||
|
for (val address : volumes.entrySet()) {
|
||||||
|
buffer.putInt((int) address.getKey().getValue());
|
||||||
|
}
|
||||||
|
buffer.position(64);
|
||||||
|
buffer.putShort(fileSystemDescriptor);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes.VolumeHeader;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInDiscBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.DiscBlockSize;
|
||||||
|
import lombok.val;
|
||||||
|
|
||||||
|
public class WiiUPartitions extends LinkedList<WiiUPartition> {
|
||||||
|
private static final long serialVersionUID = -8822482411628791495L;
|
||||||
|
|
||||||
|
public static final long LENGTH = 30720;
|
||||||
|
|
||||||
|
public static final String WUD_TMD_FILENAME = "title.tmd";
|
||||||
|
public static final String WUD_TICKET_FILENAME = "title.tik";
|
||||||
|
public static final String WUD_CERT_FILENAME = "title.cert";
|
||||||
|
|
||||||
|
public static WiiUPartitions parseData(WUDDiscReader reader, Optional<byte[]> discKey, long curOffset, long numberOfPartitions, DiscBlockSize blockSize)
|
||||||
|
throws IOException {
|
||||||
|
WiiUPartitions partitions = new WiiUPartitions();
|
||||||
|
|
||||||
|
List<WiiUPartition> tmp = new LinkedList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < numberOfPartitions; i++) {
|
||||||
|
tmp.add(WiiUPartition.parseData(reader, discKey, curOffset + (i * 128), blockSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<WiiUPartition> siPartitionOpt = tmp.stream().filter(e -> e.getVolumeID().startsWith("SI")).findFirst();
|
||||||
|
|
||||||
|
if (siPartitionOpt.isPresent()) {
|
||||||
|
WiiUPartition siPartition = siPartitionOpt.get();
|
||||||
|
for (val entry : siPartition.getVolumes().entrySet()) {
|
||||||
|
val volumeAddress = entry.getKey();
|
||||||
|
val volumeAddressInBytes = volumeAddress.getAddressInBytes();
|
||||||
|
val volumeHeader = entry.getValue();
|
||||||
|
byte[] fst = null;
|
||||||
|
if (!discKey.isPresent()) {
|
||||||
|
fst = reader.readEncryptedToByteArray(volumeAddressInBytes + volumeHeader.getFSTAddress().getAddressInBytes(), 0,
|
||||||
|
volumeHeader.getFSTSize());
|
||||||
|
} else {
|
||||||
|
fst = reader.readDecryptedToByteArray(volumeAddressInBytes + volumeHeader.getFSTAddress().getAddressInBytes(), 0, volumeHeader.getFSTSize(),
|
||||||
|
discKey.get(), null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
FST siFST = FST.parseData(fst, 0, volumeHeader.getBlockSize());
|
||||||
|
|
||||||
|
for (val child : siFST.getRootEntry().getDirChildren()) {
|
||||||
|
byte[] rawTIK = getFSTEntryAsByte(child.getFullPath() + '/' + WUD_TICKET_FILENAME, siFST, volumeAddress, volumeHeader, reader, discKey);
|
||||||
|
byte[] rawTMD = getFSTEntryAsByte(child.getFullPath() + '/' + WUD_TMD_FILENAME, siFST, volumeAddress, volumeHeader, reader, discKey);
|
||||||
|
byte[] rawCert = getFSTEntryAsByte(child.getFullPath() + '/' + WUD_CERT_FILENAME, siFST, volumeAddress, volumeHeader, reader, discKey);
|
||||||
|
|
||||||
|
String partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08));
|
||||||
|
WiiUPartition partition = tmp.stream().filter(p -> p.getVolumeID().startsWith(partitionName)).findAny()
|
||||||
|
.orElseThrow(() -> new IOException("Failed to find partition starting with " + partitionName));
|
||||||
|
|
||||||
|
WiiUGMPartition gmPartition = new WiiUGMPartition(partition, rawTIK, rawTMD, rawCert);
|
||||||
|
partitions.add(gmPartition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
val nonGMPartitions = tmp.stream().filter(e -> !e.getVolumeID().startsWith("GM")).collect(Collectors.toList());
|
||||||
|
for (val partition : nonGMPartitions) {
|
||||||
|
if (partition.getVolumes().size() != 1) {
|
||||||
|
throw new IOException("We can't handle more than one partion address yet.");
|
||||||
|
}
|
||||||
|
val volumeAddress = partition.getVolumes().keySet().iterator().next();
|
||||||
|
VolumeHeader vh = VolumeHeader.parseData(reader, volumeAddress.getAddressInBytes());
|
||||||
|
byte[] rawFST = null;
|
||||||
|
if (!discKey.isPresent()) {
|
||||||
|
rawFST = reader.readEncryptedToByteArray(volumeAddress.getAddressInBytes() + vh.getFSTAddress().getAddressInBytes(), 0, vh.getFSTSize());
|
||||||
|
} else {
|
||||||
|
rawFST = reader.readDecryptedToByteArray(volumeAddress.getAddressInBytes() + vh.getFSTAddress().getAddressInBytes(), 0, vh.getFSTSize(),
|
||||||
|
discKey.get(), null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
FST fst = FST.parseData(rawFST, 0, vh.getBlockSize());
|
||||||
|
partitions.add(new WiiUDataPartition(partition, fst));
|
||||||
|
}
|
||||||
|
|
||||||
|
return partitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getFSTEntryAsByte(String filePath, FST fst, AddressInDiscBlocks volumeAddress, VolumeHeader vh, WUDDiscReader discReader,
|
||||||
|
Optional<byte[]> discKey) throws IOException {
|
||||||
|
FileEntry entry = FSTUtils.getEntryByFullPath(fst.getRootEntry(), filePath).orElseThrow(() -> new FileNotFoundException(filePath + " was not found."));
|
||||||
|
|
||||||
|
SectionEntry info = entry.getSectionEntry();
|
||||||
|
|
||||||
|
long sectionOffsetOnDisc = volumeAddress.getAddressInBytes() + info.getAddress().getAddressInBytes();
|
||||||
|
|
||||||
|
if (!discKey.isPresent()) {
|
||||||
|
return discReader.readEncryptedToByteArray(sectionOffsetOnDisc, entry.getOffset(), (int) entry.getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculating the IV
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(0x10);
|
||||||
|
byteBuffer.position(0x08);
|
||||||
|
byte[] IV = byteBuffer.putLong(entry.getOffset() >> 16).array();
|
||||||
|
|
||||||
|
return discReader.readDecryptedToByteArray(sectionOffsetOnDisc, entry.getOffset(), (int) entry.getSize(), discKey.get(), IV, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("WiiUPartitions: " + System.lineSeparator());
|
||||||
|
for (val p : this) {
|
||||||
|
sb.append("[" + p + "]" + System.lineSeparator());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) LENGTH);
|
||||||
|
for (int i = 0; i < size(); i++) {
|
||||||
|
buffer.put(get(i).getAsBytes());
|
||||||
|
}
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class H3HashArray {
|
||||||
|
public static int LENGTH = 20;
|
||||||
|
private final byte[] H3HashArray;
|
||||||
|
|
||||||
|
public static H3HashArray parseData(WUDDiscReader reader, long offset, long t_nLength) throws IOException {
|
||||||
|
return new H3HashArray(reader.readEncryptedToByteArray(offset, 0, t_nLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "H3HashArray [H3HashArray=" + Utils.ByteArrayToString(H3HashArray) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
|
||||||
|
public class H3HashArrayList extends LinkedList<H3HashArray> {
|
||||||
|
private static final long serialVersionUID = -7607430979726338763L;
|
||||||
|
|
||||||
|
public static H3HashArrayList parseData(WUDDiscReader reader, long offset, long numberOfH3HashArray, long h3HashArrayListSize) throws IOException {
|
||||||
|
byte[] h3Data = reader.readEncryptedToByteArray(offset, 0, h3HashArrayListSize);
|
||||||
|
|
||||||
|
return parseData(h3Data, numberOfH3HashArray, h3HashArrayListSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static H3HashArrayList parseData(byte[] h3Data, long numberOfH3HashArray, long h3HashArrayListSize) throws IOException {
|
||||||
|
H3HashArrayList arrayList = new H3HashArrayList();
|
||||||
|
for (int i = 1; i < numberOfH3HashArray; i++) {
|
||||||
|
long curOffset = ByteUtils.getUnsingedIntFromBytes(Arrays.copyOfRange(h3Data, i * 4, (i + 1) * 4), 0) & 0xFFFFFFFF;
|
||||||
|
long curEnd = h3HashArrayListSize;
|
||||||
|
if (i < numberOfH3HashArray - 1) {
|
||||||
|
// If it's not the last element, the end of our .h3 is the start of the next .h3
|
||||||
|
curEnd = ByteUtils.getUnsingedIntFromBytes(Arrays.copyOfRange(h3Data, (i + 1) * 4, (i + 2) * 4), 0) & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
arrayList.add(new H3HashArray(Arrays.copyOfRange(h3Data, (int) curOffset, (int) curEnd)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayList;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.AddressInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.SizeInVolumeBlocks;
|
||||||
|
import de.mas.wiiu.jnus.utils.blocksize.VolumeBlockSize;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class VolumeHeader {
|
||||||
|
public static final int MAGIC = 0xCC93A4F5;
|
||||||
|
|
||||||
|
private VolumeBlockSize blockSize;
|
||||||
|
private SizeInVolumeBlocks volumeSize;
|
||||||
|
|
||||||
|
private long h3HashArrayListSize;
|
||||||
|
private long numberOfH3HashArray;
|
||||||
|
|
||||||
|
private long FSTSize;
|
||||||
|
private AddressInVolumeBlocks FSTAddress;
|
||||||
|
|
||||||
|
private int FSTHashMode;
|
||||||
|
|
||||||
|
private short encryptType;
|
||||||
|
|
||||||
|
private short majorVersion;
|
||||||
|
|
||||||
|
private short minorVersion;
|
||||||
|
|
||||||
|
private short expiringMajorVersion;
|
||||||
|
|
||||||
|
private H3HashArrayList h3HashArrayList;
|
||||||
|
|
||||||
|
public static VolumeHeader parseData(WUDDiscReader reader, long offset) throws IOException {
|
||||||
|
|
||||||
|
byte[] rawBytes = new byte[0];
|
||||||
|
|
||||||
|
rawBytes = reader.readEncryptedToByteArray(offset, 0, 40);
|
||||||
|
|
||||||
|
long h3HashArrayListSize = ByteUtils.getUnsingedIntFromBytes(rawBytes, 12) & 0xFFFFFFFF;
|
||||||
|
long numberOfH3HashArray = ByteUtils.getUnsingedIntFromBytes(rawBytes, 16) & 0xFFFFFFFF;
|
||||||
|
|
||||||
|
return parseData(rawBytes, () -> H3HashArrayList.parseData(reader, offset + 64, numberOfH3HashArray, h3HashArrayListSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VolumeHeader parseData(byte[] rawBytes) throws IOException {
|
||||||
|
if (rawBytes == null || rawBytes.length < 40) {
|
||||||
|
throw new IOException("Failed to read VolumeHeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
long h3HashArrayListSize = ByteUtils.getUnsingedIntFromBytes(rawBytes, 12) & 0xFFFFFFFF;
|
||||||
|
long numberOfH3HashArray = ByteUtils.getUnsingedIntFromBytes(rawBytes, 16) & 0xFFFFFFFF;
|
||||||
|
|
||||||
|
return parseData(rawBytes,
|
||||||
|
() -> H3HashArrayList.parseData(Arrays.copyOfRange(rawBytes, 64, (int) (64 + h3HashArrayListSize)), numberOfH3HashArray, h3HashArrayListSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VolumeHeader parseData(byte[] rawBytes, Callable<H3HashArrayList> h3HashSupplier) throws IOException {
|
||||||
|
if (rawBytes.length < 40) {
|
||||||
|
throw new IOException("Failed to read VolumeHeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeHeader header = new VolumeHeader();
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(rawBytes);
|
||||||
|
byte[] magicCompare = new byte[4];
|
||||||
|
buffer.get(magicCompare);
|
||||||
|
if (!Arrays.equals(magicCompare, ByteUtils.getBytesFromInt(MAGIC))) {
|
||||||
|
throw new IOException("VolumeHeader MAGIC mismatch.");
|
||||||
|
}
|
||||||
|
|
||||||
|
header.blockSize = new VolumeBlockSize(ByteUtils.getUnsingedIntFromBytes(rawBytes, 4) & 0xFFFFFFFF);
|
||||||
|
header.volumeSize = new SizeInVolumeBlocks(header.blockSize, ByteUtils.getUnsingedIntFromBytes(rawBytes, 8) & 0xFFFFFFFF);
|
||||||
|
header.h3HashArrayListSize = ByteUtils.getUnsingedIntFromBytes(rawBytes, 12) & 0xFFFFFFFF;
|
||||||
|
header.numberOfH3HashArray = ByteUtils.getUnsingedIntFromBytes(rawBytes, 16) & 0xFFFFFFFF;
|
||||||
|
header.FSTSize = ByteUtils.getUnsingedIntFromBytes(rawBytes, 20) & 0xFFFFFFFF;
|
||||||
|
header.FSTAddress = new AddressInVolumeBlocks(header.blockSize, ByteUtils.getUnsingedIntFromBytes(rawBytes, 24) & 0xFFFFFFFF);
|
||||||
|
header.FSTHashMode = ByteUtils.getByteFromBytes(rawBytes, 36);
|
||||||
|
header.encryptType = ByteUtils.getByteFromBytes(rawBytes, 37);
|
||||||
|
header.majorVersion = ByteUtils.getByteFromBytes(rawBytes, 38);
|
||||||
|
header.minorVersion = ByteUtils.getByteFromBytes(rawBytes, 39);
|
||||||
|
header.expiringMajorVersion = ByteUtils.getByteFromBytes(rawBytes, 40);
|
||||||
|
try {
|
||||||
|
header.h3HashArrayList = h3HashSupplier.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (e instanceof IOException) {
|
||||||
|
throw (IOException) e;
|
||||||
|
} else {
|
||||||
|
// This should never happen.
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "VolumeHeader [blockSize=" + blockSize + ", volumeSize=" + volumeSize + ", h3HashArrayListSize=" + h3HashArrayListSize + ", numberOfH3HashArray="
|
||||||
|
+ numberOfH3HashArray + ", FSTSize=" + FSTSize + ", FSTAddress=" + FSTAddress + ", FSTHashMode=" + FSTHashMode + ", encryptType=" + encryptType
|
||||||
|
+ ", majorVersion=" + majorVersion + ", minorVersion=" + minorVersion + ", expiringMajorVersion=" + expiringMajorVersion + ", byteSource="
|
||||||
|
+ ", h3HashArrayList=" + h3HashArrayList + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) blockSize.getBlockSize());
|
||||||
|
buffer.put(ByteUtils.getBytesFromInt(MAGIC));
|
||||||
|
buffer.putInt((int) blockSize.getBlockSize());
|
||||||
|
buffer.putInt((int) volumeSize.getValue());
|
||||||
|
buffer.putInt((int) h3HashArrayListSize);
|
||||||
|
buffer.putInt((int) numberOfH3HashArray);
|
||||||
|
buffer.putInt((int) FSTSize);
|
||||||
|
buffer.putInt((int) FSTAddress.getValue());
|
||||||
|
buffer.put((byte) FSTHashMode);
|
||||||
|
buffer.put((byte) encryptType);
|
||||||
|
buffer.put((byte) majorVersion);
|
||||||
|
buffer.put((byte) minorVersion);
|
||||||
|
buffer.put((byte) expiringMajorVersion);
|
||||||
|
buffer.position(64);
|
||||||
|
if (h3HashArrayListSize > 0 || numberOfH3HashArray > 0) {
|
||||||
|
throw new IllegalArgumentException("Support for packing with h3 hashes is missing.");
|
||||||
|
}
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.header;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.WiiUContentsInformation;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUDiscHeader {
|
||||||
|
private static final long LENGTH = 131072L;
|
||||||
|
WiiUManufactorDiscID manufactorDiscID;
|
||||||
|
WiiUDiscID discID;
|
||||||
|
WiiUContentsInformation contentsInformation;
|
||||||
|
|
||||||
|
public static WiiUDiscHeader parseData(WUDDiscReader reader, Optional<byte[]> discKey) throws IOException {
|
||||||
|
WiiUDiscHeader header = new WiiUDiscHeader();
|
||||||
|
long curOffset = 0;
|
||||||
|
header.setManufactorDiscID(WiiUManufactorDiscID.parseData(reader, 0));
|
||||||
|
curOffset += WiiUManufactorDiscID.LENGTH;
|
||||||
|
header.setDiscID(WiiUDiscID.parseData(reader, curOffset));
|
||||||
|
curOffset += WiiUDiscID.LENGTH;
|
||||||
|
header.setContentsInformation(WiiUContentsInformation.parseData(reader, discKey, curOffset));
|
||||||
|
|
||||||
|
curOffset += WiiUContentsInformation.LENGTH;
|
||||||
|
|
||||||
|
if (curOffset != LENGTH) {
|
||||||
|
throw new IOException("Length mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "WiiUDiscHeader [manufactorDiscID=" + manufactorDiscID + ", discID=" + discID + ", contentsInformation=" + contentsInformation + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate((int) LENGTH);
|
||||||
|
buffer.put(manufactorDiscID.getAsBytes());
|
||||||
|
buffer.put(discID.getAsBytes());
|
||||||
|
buffer.put(contentsInformation.getAsBytes());
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.header;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUDiscID {
|
||||||
|
public static final int MAGIC = 0xCC549EB9;
|
||||||
|
public static final int LENGTH = 32768;
|
||||||
|
|
||||||
|
private final byte majorVersion;
|
||||||
|
private final byte minorVersion;
|
||||||
|
private final String footprint;
|
||||||
|
|
||||||
|
public static WiiUDiscID parseData(WUDDiscReader reader, long offset) throws IOException {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
if (reader.readEncryptedToStream(baos, offset, LENGTH) != LENGTH) {
|
||||||
|
throw new IOException("Failed to read DiscId");
|
||||||
|
}
|
||||||
|
byte[] rawBytes = baos.toByteArray();
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(rawBytes);
|
||||||
|
byte[] magicCompare = new byte[4];
|
||||||
|
buffer.get(magicCompare);
|
||||||
|
if (!Arrays.equals(magicCompare, ByteUtils.getBytesFromInt(MAGIC))) {
|
||||||
|
throw new IOException("DiscId MAGIC mismatch.");
|
||||||
|
}
|
||||||
|
byte majorVersion = buffer.get();
|
||||||
|
byte minorVersion = buffer.get();
|
||||||
|
|
||||||
|
byte[] strRaw = Arrays.copyOfRange(rawBytes, 32, 32 + 64);
|
||||||
|
for (int i = 0; i < strRaw.length; i++) {
|
||||||
|
if (strRaw[i] == '\0') {
|
||||||
|
strRaw = Arrays.copyOf(strRaw, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String footprint = new String(strRaw, Charset.forName("ISO-8859-1"));
|
||||||
|
|
||||||
|
return new WiiUDiscID(majorVersion, minorVersion, footprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(LENGTH);
|
||||||
|
buffer.put(ByteUtils.getBytesFromInt(MAGIC));
|
||||||
|
buffer.put(majorVersion);
|
||||||
|
buffer.put(minorVersion);
|
||||||
|
buffer.put(footprint.getBytes());
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package de.mas.wiiu.jnus.implementations.wud.header;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WiiUManufactorDiscID {
|
||||||
|
public final static long LENGTH = 65536;
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
static WiiUManufactorDiscID parseData(WUDDiscReader reader, long offset) throws IOException {
|
||||||
|
byte[] data = reader.readEncryptedToByteArray(offset, 0, LENGTH);
|
||||||
|
if (data.length != LENGTH) {
|
||||||
|
throw new IOException("Failed to read ManufactorDiscID");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WiiUManufactorDiscID(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "WiiUManufactorDiscID [data=" + Utils.ByteArrayToString(data) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAsBytes() {
|
||||||
|
return ByteBuffer.wrap(data).array();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public class WUDDataPartition extends WUDPartition {
|
|
||||||
@Getter private final FST FST;
|
|
||||||
|
|
||||||
public WUDDataPartition(String partitionName, long partitionOffset, FST curFST) {
|
|
||||||
super(partitionName, partitionOffset);
|
|
||||||
this.FST = curFST;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class WUDGamePartition extends WUDPartition {
|
|
||||||
|
|
||||||
private final GamePartitionHeader partitionHeader;
|
|
||||||
|
|
||||||
private final TMD tmd;
|
|
||||||
private final byte[] rawTMD;
|
|
||||||
private final byte[] rawCert;
|
|
||||||
private final byte[] rawTicket;
|
|
||||||
|
|
||||||
public WUDGamePartition(String partitionName, long partitionOffset, GamePartitionHeader partitionHeader, byte[] rawTMD, byte[] rawCert, byte[] rawTicket)
|
|
||||||
throws ParseException {
|
|
||||||
super(partitionName, partitionOffset);
|
|
||||||
this.partitionHeader = partitionHeader;
|
|
||||||
this.rawTMD = rawTMD;
|
|
||||||
this.tmd = TMD.parseTMD(rawTMD);
|
|
||||||
this.rawCert = rawCert;
|
|
||||||
this.rawTicket = rawTicket;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class WUDInfo {
|
|
||||||
@Getter private final byte[] titleKey;
|
|
||||||
private final WUDDiscReader WUDDiscReader;
|
|
||||||
private final List<WUDPartition> partitions = new ArrayList<>();
|
|
||||||
|
|
||||||
public List<WUDGamePartition> getGamePartitions() {
|
|
||||||
return partitions.stream().filter(p -> p instanceof WUDGamePartition).map(p -> (WUDGamePartition) p).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<WUDDataPartition> getDataPartitions() {
|
|
||||||
return partitions.stream().filter(p -> p instanceof WUDDataPartition).map(p -> ((WUDDataPartition) p)).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.Settings;
|
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
|
||||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
|
||||||
import de.mas.wiiu.jnus.utils.Utils;
|
|
||||||
import lombok.val;
|
|
||||||
import lombok.extern.java.Log;
|
|
||||||
|
|
||||||
@Log
|
|
||||||
// TODO: reduce magic numbers
|
|
||||||
public final class WUDInfoParser {
|
|
||||||
public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
|
|
||||||
public static byte[] PARTITION_START_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0x93, (byte) 0xA4, (byte) 0xF5 };
|
|
||||||
public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
|
|
||||||
public final static int SECTOR_SIZE = 0x8000;
|
|
||||||
public final static int PARTITION_TOC_OFFSET = 0x800;
|
|
||||||
public final static int PARTITION_TOC_ENTRY_SIZE = 0x80;
|
|
||||||
|
|
||||||
public static final String WUD_TMD_FILENAME = "title.tmd";
|
|
||||||
public static final String WUD_TICKET_FILENAME = "title.tik";
|
|
||||||
public static final String WUD_CERT_FILENAME = "title.cert";
|
|
||||||
|
|
||||||
private WUDInfoParser() {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WUDInfo createAndLoad(WUDDiscReader discReader, byte[] titleKey) throws IOException, ParseException {
|
|
||||||
WUDInfo result = new WUDInfo(titleKey, discReader);
|
|
||||||
|
|
||||||
byte[] PartitionTocBlock = readFromDisc(result, Settings.WIIU_DECRYPTED_AREA_OFFSET, SECTOR_SIZE);
|
|
||||||
|
|
||||||
// verify DiscKey before proceeding
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(PartitionTocBlock, 0, 4), DECRYPTED_AREA_SIGNATURE)) {
|
|
||||||
// log.info("Decryption of PartitionTocBlock failed");
|
|
||||||
throw new ParseException("Decryption of PartitionTocBlock failed", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.getPartitions().addAll(parsePartitions(result, PartitionTocBlock));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<WUDPartition> parsePartitions(WUDInfo wudInfo, byte[] partitionTocBlock) throws IOException, ParseException {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(partitionTocBlock.length);
|
|
||||||
|
|
||||||
buffer.order(ByteOrder.BIG_ENDIAN);
|
|
||||||
buffer.put(partitionTocBlock);
|
|
||||||
buffer.position(0);
|
|
||||||
|
|
||||||
int partitionCount = (int) ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, 0x1C, ByteOrder.BIG_ENDIAN);
|
|
||||||
|
|
||||||
Map<String, Long> internalPartitions = new HashMap<>();
|
|
||||||
|
|
||||||
// populate partition information from decrypted TOC
|
|
||||||
for (int i = 0; i < partitionCount; i++) {
|
|
||||||
int offset = (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE));
|
|
||||||
byte[] partitionIdentifier = Arrays.copyOfRange(partitionTocBlock, offset, offset + 0x19);
|
|
||||||
int j = 0;
|
|
||||||
for (j = 0; j < partitionIdentifier.length; j++) {
|
|
||||||
if (partitionIdentifier[j] == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String partitionName = new String(Arrays.copyOfRange(partitionIdentifier, 0, j));
|
|
||||||
|
|
||||||
// calculate partition offset from decrypted TOC
|
|
||||||
long offsetInSector = ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE) + 0x20),
|
|
||||||
ByteOrder.BIG_ENDIAN);
|
|
||||||
|
|
||||||
long partitionOffset = (offsetInSector * (long) SECTOR_SIZE);
|
|
||||||
|
|
||||||
internalPartitions.put(partitionName, partitionOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, WUDPartition> partitionsResult = new HashMap<>();
|
|
||||||
|
|
||||||
val siPartitionOpt = internalPartitions.entrySet().stream().filter(e -> e.getKey().startsWith("SI")).findFirst();
|
|
||||||
if (siPartitionOpt.isPresent()) {
|
|
||||||
val siPartitionPair = siPartitionOpt.orElseThrow(() -> new ParseException("SI partition not found.", 0));
|
|
||||||
|
|
||||||
// siPartition
|
|
||||||
long siPartitionOffset = siPartitionPair.getValue();
|
|
||||||
|
|
||||||
byte[] partitionHeaderData = readFromDisc(wudInfo, false, siPartitionOffset, 0x20);
|
|
||||||
if (!Arrays.equals(Arrays.copyOf(partitionHeaderData, 0x4), PARTITION_START_SIGNATURE)) {
|
|
||||||
throw new ParseException("Failed to get the partition data of the SI partition.", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
long headerSize = ByteUtils.getUnsingedIntFromBytes(partitionHeaderData, 0x04);
|
|
||||||
|
|
||||||
long absoluteFSTOffset = siPartitionOffset + headerSize;
|
|
||||||
long FSTSize = ByteUtils.getUnsingedIntFromBytes(partitionHeaderData, 0x14);
|
|
||||||
|
|
||||||
byte[] fileTableBlock = readFromDisc(wudInfo, absoluteFSTOffset, FSTSize);
|
|
||||||
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(fileTableBlock, 0, 4), WUDInfoParser.PARTITION_FILE_TABLE_SIGNATURE)) {
|
|
||||||
log.info("FST Decrpytion failed");
|
|
||||||
throw new ParseException("Failed to decrypt the FST of the SI partition.", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
FST siFST = FST.parseFST(fileTableBlock);
|
|
||||||
|
|
||||||
for (val dirChilden : siFST.getRoot().getDirChildren()) {
|
|
||||||
// The SI partition contains the tmd, cert and tik for every GM partition.
|
|
||||||
byte[] rawTIK = getFSTEntryAsByte(dirChilden.getFullPath() + '/' + WUD_TICKET_FILENAME, siPartitionOffset, headerSize, siFST,
|
|
||||||
wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
|
||||||
byte[] rawTMD = getFSTEntryAsByte(dirChilden.getFullPath() + '/' + WUD_TMD_FILENAME, siPartitionOffset, headerSize, siFST,
|
|
||||||
wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
|
||||||
byte[] rawCert = getFSTEntryAsByte(dirChilden.getFullPath() + '/' + WUD_CERT_FILENAME, siPartitionOffset, headerSize, siFST,
|
|
||||||
wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
|
||||||
|
|
||||||
String partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08));
|
|
||||||
|
|
||||||
val curPartitionOpt = internalPartitions.entrySet().stream().filter(e -> e.getKey().startsWith(partitionName)).findFirst();
|
|
||||||
val curPartitionPair = curPartitionOpt.orElseThrow(() -> new ParseException("partition not found.", 0));
|
|
||||||
long curPartitionOffset = curPartitionPair.getValue();
|
|
||||||
|
|
||||||
byte[] curPartitionHeaderMeta = readFromDisc(wudInfo, false, curPartitionOffset, 0x20);
|
|
||||||
|
|
||||||
if (!Arrays.equals(Arrays.copyOf(curPartitionHeaderMeta, 0x4), PARTITION_START_SIGNATURE)) {
|
|
||||||
throw new ParseException("Failed to decrypt the SI partition.", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
long curHeaderSize = ByteUtils.getUnsingedIntFromBytes(curPartitionHeaderMeta, 0x04);
|
|
||||||
|
|
||||||
byte[] header = wudInfo.getWUDDiscReader().readEncryptedToByteArray(curPartitionOffset, 0, curHeaderSize);
|
|
||||||
|
|
||||||
GamePartitionHeader partitionHeader = GamePartitionHeader.parseHeader(header);
|
|
||||||
|
|
||||||
WUDGamePartition curPartition = new WUDGamePartition(curPartitionPair.getKey(), curPartitionOffset + curHeaderSize, partitionHeader, rawTMD,
|
|
||||||
rawCert, rawTIK);
|
|
||||||
|
|
||||||
partitionsResult.put(curPartitionPair.getKey(), curPartition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataPartitions = internalPartitions.entrySet().stream().filter(e -> !e.getKey().startsWith("GM")).collect(Collectors.toList());
|
|
||||||
for (val dataPartition : dataPartitions) {
|
|
||||||
String curPartionName = dataPartition.getKey();
|
|
||||||
long partitionOffset = dataPartition.getValue();
|
|
||||||
|
|
||||||
byte[] partitionHeaderData = readFromDisc(wudInfo, false, partitionOffset, 0x20);
|
|
||||||
|
|
||||||
if (!Arrays.equals(Arrays.copyOf(partitionHeaderData, 0x4), PARTITION_START_SIGNATURE)) {
|
|
||||||
throw new ParseException("Failed to decrypt the " + curPartionName + " partition.", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
long headerSize = ByteUtils.getUnsingedIntFromBytes(partitionHeaderData, 0x04);
|
|
||||||
|
|
||||||
long absoluteFSTOffset = partitionOffset + headerSize;
|
|
||||||
long FSTSize = ByteUtils.getUnsingedIntFromBytes(partitionHeaderData, 0x14);
|
|
||||||
|
|
||||||
byte[] curFileTableBlock = readFromDisc(wudInfo, absoluteFSTOffset, FSTSize);
|
|
||||||
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(curFileTableBlock, 0, 4), WUDInfoParser.PARTITION_FILE_TABLE_SIGNATURE)) {
|
|
||||||
throw new IOException("FST Decrpytion failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
FST curFST = FST.parseFST(curFileTableBlock);
|
|
||||||
|
|
||||||
WUDDataPartition curDataPartition = new WUDDataPartition(curPartionName, partitionOffset + headerSize, curFST);
|
|
||||||
|
|
||||||
partitionsResult.put(curPartionName, curDataPartition);
|
|
||||||
}
|
|
||||||
|
|
||||||
return partitionsResult.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readFromDisc(WUDInfo wudInfo, long offset, long size) throws IOException {
|
|
||||||
return readFromDisc(wudInfo, wudInfo.getTitleKey() != null, offset, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readFromDisc(WUDInfo wudInfo, boolean decrypt, long offset, long size) throws IOException {
|
|
||||||
if (!decrypt) {
|
|
||||||
return wudInfo.getWUDDiscReader().readEncryptedToByteArray(offset, 0, size);
|
|
||||||
} else {
|
|
||||||
return wudInfo.getWUDDiscReader().readDecryptedToByteArray(offset, 0, size, wudInfo.getTitleKey(), null, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] getFSTEntryAsByte(String filePath, long partitionOffset, long headerSize, FST fst, WUDDiscReader discReader, byte[] key)
|
|
||||||
throws IOException {
|
|
||||||
FSTEntry entry = FSTUtils.getEntryByFullPath(fst.getRoot(), filePath).orElseThrow(() -> new FileNotFoundException(filePath + " was not found."));
|
|
||||||
|
|
||||||
ContentFSTInfo info = fst.getContentFSTInfos().get((int) entry.getContentIndex());
|
|
||||||
|
|
||||||
if (key == null) {
|
|
||||||
return discReader.readEncryptedToByteArray(headerSize + partitionOffset + (long) info.getOffset(), entry.getFileOffset(),
|
|
||||||
(int) entry.getFileSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculating the IV
|
|
||||||
ByteBuffer byteBuffer = ByteBuffer.allocate(0x10);
|
|
||||||
byteBuffer.position(0x08);
|
|
||||||
byte[] IV = byteBuffer.putLong(entry.getFileOffset() >> 16).array();
|
|
||||||
|
|
||||||
return discReader.readDecryptedToByteArray(headerSize + partitionOffset + info.getOffset(), entry.getFileOffset(), (int) entry.getFileSize(), key, IV,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
/****************************************************************************
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
****************************************************************************/
|
|
||||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public abstract class WUDPartition {
|
|
||||||
private final String partitionName;
|
|
||||||
private final long partitionOffset;
|
|
||||||
}
|
|
@ -72,8 +72,7 @@ public abstract class WUDDiscReader {
|
|||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV,
|
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV, boolean useFixedIV) throws IOException {
|
||||||
boolean useFixedIV) throws IOException {
|
|
||||||
PipedOutputStream out = new PipedOutputStream();
|
PipedOutputStream out = new PipedOutputStream();
|
||||||
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000);
|
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package de.mas.wiiu.jnus.implementations.wud.wumad;
|
package de.mas.wiiu.jnus.implementations.wud.wumad;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
public class WumadDataPartition extends WumadPartition {
|
public class WumadDataPartition extends WumadPartition {
|
||||||
|
@ -2,27 +2,26 @@ package de.mas.wiiu.jnus.implementations.wud.wumad;
|
|||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
import de.mas.wiiu.jnus.entities.TMD.TitleMetaData;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes.VolumeHeader;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class WumadGamePartition extends WumadPartition {
|
public class WumadGamePartition extends WumadPartition {
|
||||||
private final GamePartitionHeader partitionHeader;
|
private final VolumeHeader partitionHeader;
|
||||||
|
|
||||||
private final TMD tmd;
|
private final TitleMetaData 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 WumadGamePartition(String partitionName, GamePartitionHeader partitionHeader, byte[] rawTMD, byte[] rawCert, byte[] rawTicket)
|
public WumadGamePartition(String partitionName, VolumeHeader partitionHeader, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) throws ParseException {
|
||||||
throws ParseException {
|
|
||||||
super(partitionName);
|
super(partitionName);
|
||||||
this.partitionHeader = partitionHeader;
|
this.partitionHeader = partitionHeader;
|
||||||
this.rawTMD = rawTMD;
|
this.rawTMD = rawTMD;
|
||||||
this.tmd = TMD.parseTMD(rawTMD);
|
this.tmd = TitleMetaData.parseTMD(rawTMD);
|
||||||
this.rawCert = rawCert;
|
this.rawCert = rawCert;
|
||||||
this.rawTicket = rawTicket;
|
this.rawTicket = rawTicket;
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,10 @@ import javax.xml.parsers.ParserConfigurationException;
|
|||||||
|
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
|
||||||
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes.VolumeHeader;
|
||||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
@ -61,15 +62,15 @@ public class WumadParser {
|
|||||||
Map<String, List<ZipEntry>> allPartitions = zipFile.stream().filter(e -> e.getName().startsWith("p"))
|
Map<String, List<ZipEntry>> allPartitions = zipFile.stream().filter(e -> e.getName().startsWith("p"))
|
||||||
.collect(Collectors.groupingBy(e -> e.getName().substring(1, 3)));
|
.collect(Collectors.groupingBy(e -> e.getName().substring(1, 3)));
|
||||||
|
|
||||||
Map<String, FSTEntry> gamepartitions = new HashMap<>();
|
Map<String, DirectoryEntry> gamepartitions = new HashMap<>();
|
||||||
|
|
||||||
// If we have a SI partition, let parse the FST to get all game partitions.
|
// If we have a SI partition, let parse the FST to get all game partitions.
|
||||||
ZipEntry siFST = zipFile.getEntry(SI_FST_FILENAME);
|
ZipEntry siFST = zipFile.getEntry(SI_FST_FILENAME);
|
||||||
if (siFST != null) {
|
if (siFST != null) {
|
||||||
byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(siFST), (int) siFST.getSize());
|
byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(siFST), (int) siFST.getSize());
|
||||||
|
|
||||||
FST parsedFST = FST.parseFST(fstBytes);
|
FST parsedFST = FST.parseData(fstBytes);
|
||||||
gamepartitions.putAll(parsedFST.getRoot().getDirChildren().stream().collect(Collectors.toMap(e -> e.getFilename(), e -> e)));
|
gamepartitions.putAll(parsedFST.getRootEntry().getDirChildren().stream().collect(Collectors.toMap(e -> e.getName(), e -> e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// process all game partitions. Remove the partitions from the "all partitions" list on success.
|
// process all game partitions. Remove the partitions from the "all partitions" list on success.
|
||||||
@ -87,7 +88,7 @@ public class WumadParser {
|
|||||||
|
|
||||||
byte[] header = StreamUtils.getBytesFromStream(zipFile.getInputStream(headerEntry), (int) headerEntry.getSize());
|
byte[] header = StreamUtils.getBytesFromStream(zipFile.getInputStream(headerEntry), (int) headerEntry.getSize());
|
||||||
|
|
||||||
WumadGamePartition curPartition = new WumadGamePartition(e.getKey(), GamePartitionHeader.parseHeader(header), rawTMD, rawCert, rawTIK);
|
WumadGamePartition curPartition = new WumadGamePartition(e.getKey(), VolumeHeader.parseData(header), rawTMD, rawCert, rawTIK);
|
||||||
|
|
||||||
result.getPartitions().add(curPartition);
|
result.getPartitions().add(curPartition);
|
||||||
allPartitions.remove(e.getKey());
|
allPartitions.remove(e.getKey());
|
||||||
@ -99,7 +100,7 @@ public class WumadParser {
|
|||||||
|
|
||||||
byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(fstEntry), (int) fstEntry.getSize());
|
byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(fstEntry), (int) fstEntry.getSize());
|
||||||
|
|
||||||
FST parsedFST = FST.parseFST(fstBytes);
|
FST parsedFST = FST.parseData(fstBytes);
|
||||||
|
|
||||||
WumadDataPartition curPartition = new WumadDataPartition(e.getKey(), parsedFST);
|
WumadDataPartition curPartition = new WumadDataPartition(e.getKey(), parsedFST);
|
||||||
|
|
||||||
@ -115,16 +116,16 @@ public class WumadParser {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<byte[]> getFSTEntryAsByte(String filePath, FSTEntry dirRoot, ZipFile zipFile, ZipEntry data) throws IOException {
|
private static Optional<byte[]> getFSTEntryAsByte(String filePath, DirectoryEntry dirRoot, ZipFile zipFile, ZipEntry data) throws IOException {
|
||||||
Optional<FSTEntry> entryOpt = FSTUtils.getEntryByFullPath(dirRoot, filePath);
|
Optional<FileEntry> entryOpt = FSTUtils.getEntryByFullPath(dirRoot, filePath);
|
||||||
if (!entryOpt.isPresent()) {
|
if (!entryOpt.isPresent()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
FSTEntry entry = entryOpt.get();
|
FileEntry entry = entryOpt.get();
|
||||||
|
|
||||||
InputStream in = zipFile.getInputStream(data);
|
InputStream in = zipFile.getInputStream(data);
|
||||||
|
|
||||||
StreamUtils.skipExactly(in, entry.getFileOffset());
|
StreamUtils.skipExactly(in, entry.getOffset());
|
||||||
return Optional.of(StreamUtils.getBytesFromStream(in, (int) entry.getFileSize()));
|
return Optional.of(StreamUtils.getBytesFromStream(in, (int) entry.getSize()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,18 @@ public interface ContentDecryptor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param in InputStream of the Encrypted Data with hashed
|
* @param in
|
||||||
* @param out OutputStream of the decrypted data with hashes
|
* InputStream of the Encrypted Data with hashed
|
||||||
* @param offset absolute offset in this Content stream
|
* @param out
|
||||||
* @param size size of the payload that will be written to the outputstream
|
* OutputStream of the decrypted data with hashes
|
||||||
* @param payloadOffset relative offset to the start of the inputstream
|
* @param offset
|
||||||
* @param h3_hashes level 3 hashes of the content file
|
* absolute offset in this Content stream
|
||||||
|
* @param size
|
||||||
|
* size of the payload that will be written to the outputstream
|
||||||
|
* @param payloadOffset
|
||||||
|
* relative offset to the start of the inputstream
|
||||||
|
* @param h3_hashes
|
||||||
|
* level 3 hashes of the content file
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
|
@ -10,6 +10,7 @@ public interface ContentEncryptor {
|
|||||||
|
|
||||||
long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException;
|
long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException;
|
||||||
|
|
||||||
long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache) throws IOException;
|
long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,19 +22,20 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PipedOutputStream;
|
import java.io.PipedOutputStream;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
|
||||||
import de.mas.wiiu.jnus.utils.PipedInputStreamWithException;
|
import de.mas.wiiu.jnus.utils.PipedInputStreamWithException;
|
||||||
|
|
||||||
public interface FSTDataProvider {
|
public interface FSTDataProvider {
|
||||||
public String getName();
|
public String getName();
|
||||||
|
|
||||||
public FSTEntry getRoot();
|
public RootEntry getRoot();
|
||||||
|
|
||||||
default public byte[] readFile(FSTEntry entry) throws IOException {
|
default public byte[] readFile(FileEntry entry) throws IOException {
|
||||||
return readFile(entry, 0, entry.getFileSize());
|
return readFile(entry, 0, entry.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
default public byte[] readFile(FSTEntry entry, long offset, long size) throws IOException {
|
default public byte[] readFile(FileEntry entry, long offset, long size) throws IOException {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
readFileToStream(out, entry, offset, size);
|
readFileToStream(out, entry, offset, size);
|
||||||
@ -42,11 +43,11 @@ public interface FSTDataProvider {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
default public InputStream readFileAsStream(FSTEntry entry) throws IOException {
|
default public InputStream readFileAsStream(FileEntry entry) throws IOException {
|
||||||
return readFileAsStream(entry, 0, entry.getFileSize());
|
return readFileAsStream(entry, 0, entry.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
default public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
|
default public InputStream readFileAsStream(FileEntry entry, long offset, long size) throws IOException {
|
||||||
PipedOutputStream out = new PipedOutputStream();
|
PipedOutputStream out = new PipedOutputStream();
|
||||||
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000);
|
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000);
|
||||||
|
|
||||||
@ -67,14 +68,14 @@ public interface FSTDataProvider {
|
|||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
default public long readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
|
default public long readFileToStream(OutputStream out, FileEntry entry) throws IOException {
|
||||||
return readFileToStream(out, entry, 0, entry.getFileSize());
|
return readFileToStream(out, entry, 0, entry.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
default public long readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
|
default public long readFileToStream(OutputStream out, FileEntry entry, long offset) throws IOException {
|
||||||
return readFileToStream(out, entry, offset, entry.getFileSize());
|
return readFileToStream(out, entry, offset, entry.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException;
|
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
|
|
||||||
public interface NUSDataProcessor {
|
public interface NUSDataProcessor {
|
||||||
@ -50,7 +50,7 @@ public interface NUSDataProcessor {
|
|||||||
default public byte[] readDecryptedContent(Content c, long offset, long size) throws IOException {
|
default public byte[] readDecryptedContent(Content c, long offset, long size) throws IOException {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
long len = readDecryptedContentToStream(out, c, offset, size);
|
long len = readDecryptedContentToStream(out, c, offset, size);
|
||||||
if(len < 0) {
|
if (len < 0) {
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
@ -83,7 +83,7 @@ public interface NUSDataProcessor {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
long len = readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash);
|
long len = readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash);
|
||||||
if(len < 0) {
|
if (len < 0) {
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ 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.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
|
|
||||||
public interface NUSDataProvider {
|
public interface NUSDataProvider {
|
||||||
|
@ -3,14 +3,12 @@ package de.mas.wiiu.jnus.interfaces;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface TriFunction<A, B, C, R> {
|
public interface TriFunction<A, B, C, R> {
|
||||||
|
|
||||||
R apply(A a, B b, C c);
|
R apply(A a, B b, C c);
|
||||||
|
|
||||||
default <V> TriFunction<A, B, C, V> andThen(
|
default <V> TriFunction<A, B, C, V> andThen(Function<? super R, ? extends V> after) {
|
||||||
Function<? super R, ? extends V> after) {
|
|
||||||
Objects.requireNonNull(after);
|
Objects.requireNonNull(after);
|
||||||
return (A a, B b, C c) -> after.apply(apply(a, b, c));
|
return (A a, B b, C c) -> after.apply(apply(a, b, c));
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
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.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.java.Log;
|
import lombok.extern.java.Log;
|
||||||
|
@ -25,20 +25,22 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
import de.mas.wiiu.jnus.entities.FST.FST;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
|
||||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.nodeentry.NodeEntry;
|
||||||
|
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
|
||||||
public class FSTUtils {
|
public class FSTUtils {
|
||||||
public static Optional<FSTEntry> getFSTEntryByFullPath(FSTEntry root, String givenFullPath) {
|
public static Optional<NodeEntry> getFSTEntryByFullPath(DirectoryEntry root, String givenFullPath) {
|
||||||
String fullPath = givenFullPath.replace(File.separator, "/");
|
String fullPath = givenFullPath.replace(File.separator, "/");
|
||||||
if (!fullPath.startsWith("/")) {
|
if (!fullPath.startsWith("/")) {
|
||||||
fullPath = "/" + fullPath;
|
fullPath = "/" + fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
String dirPath = FilenameUtils.getFullPathNoEndSeparator(fullPath);
|
String dirPath = FilenameUtils.getFullPathNoEndSeparator(fullPath);
|
||||||
Optional<FSTEntry> pathOpt = Optional.of(root);
|
Optional<DirectoryEntry> pathOpt = Optional.of(root);
|
||||||
if (!dirPath.equals("/")) {
|
if (!dirPath.equals("/")) {
|
||||||
pathOpt = getFileEntryDir(root, dirPath);
|
pathOpt = getFileEntryDir(root, dirPath);
|
||||||
}
|
}
|
||||||
@ -48,7 +50,7 @@ public class FSTUtils {
|
|||||||
return pathOpt.flatMap(e -> e.getChildren().stream().filter(c -> c.getFullPath().equals(path)).findAny());
|
return pathOpt.flatMap(e -> e.getChildren().stream().filter(c -> c.getFullPath().equals(path)).findAny());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<FSTEntry> getFileEntryDir(FSTEntry curEntry, String string) {
|
public static Optional<DirectoryEntry> getFileEntryDir(DirectoryEntry curEntry, String string) {
|
||||||
string = string.replace(File.separator, "/");
|
string = string.replace(File.separator, "/");
|
||||||
|
|
||||||
// We add the "/" at the end so we don't get false results when using the "startWith" function.
|
// We add the "/" at the end so we don't get false results when using the "startWith" function.
|
||||||
@ -71,15 +73,15 @@ public class FSTUtils {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<FSTEntry> getEntryByFullPath(FSTEntry root, String filePath) {
|
public static Optional<FileEntry> getEntryByFullPath(DirectoryEntry root, String filePath) {
|
||||||
for (FSTEntry cur : root.getFileChildren()) {
|
for (FileEntry cur : root.getFileChildren()) {
|
||||||
if (cur.getFullPath().equals(filePath)) {
|
if (cur.getFullPath().equals(filePath)) {
|
||||||
return Optional.of(cur);
|
return Optional.of(cur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (FSTEntry cur : root.getDirChildren()) {
|
for (DirectoryEntry cur : root.getDirChildren()) {
|
||||||
Optional<FSTEntry> res = getEntryByFullPath(cur, filePath);
|
Optional<FileEntry> res = getEntryByFullPath(cur, filePath);
|
||||||
if (res.isPresent()) {
|
if (res.isPresent()) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -87,75 +89,75 @@ public class FSTUtils {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<FSTEntry> getChildOfDirectory(FSTEntry root, String filename) {
|
public static Optional<NodeEntry> getChildOfDirectory(DirectoryEntry root, String filename) {
|
||||||
for (FSTEntry cur : root.getChildren()) {
|
for (NodeEntry cur : root.getChildren()) {
|
||||||
if (cur.getFilename().equalsIgnoreCase(filename)) {
|
if (cur.getName().equalsIgnoreCase(filename)) {
|
||||||
return Optional.of(cur);
|
return Optional.of(cur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FSTEntry> getFSTEntriesByRegEx(FSTEntry root, String string) {
|
public static List<FileEntry> getFSTEntriesByRegEx(DirectoryEntry root, String string) {
|
||||||
return getFSTEntriesByRegEx(root, string, false);
|
return getFSTEntriesByRegEx(root, string, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FSTEntry> getFSTEntriesByRegEx(FSTEntry entry, String regEx, boolean allowNotInPackage) {
|
public static List<FileEntry> getFSTEntriesByRegEx(DirectoryEntry entry, String regEx, boolean allowNotInPackage) {
|
||||||
Pattern p = Pattern.compile(regEx);
|
Pattern p = Pattern.compile(regEx);
|
||||||
return getFSTEntriesByRegExStream(entry, p, allowNotInPackage).collect(Collectors.toList());
|
return getFSTEntriesByRegExStream(entry, p, allowNotInPackage).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<FSTEntry> getFSTEntriesByRegExStream(FSTEntry entry, Pattern p, boolean allowNotInPackage) {
|
private static Stream<FileEntry> getFSTEntriesByRegExStream(DirectoryEntry entry, Pattern p, boolean allowNotInPackage) {
|
||||||
return entry.getChildren().stream()//
|
return entry.getChildren().stream()//
|
||||||
.filter(e -> allowNotInPackage || !e.isNotInPackage()) //
|
.filter(e -> allowNotInPackage || !e.isLink()) //
|
||||||
.flatMap(e -> {
|
.flatMap(e -> {
|
||||||
if (!e.isDir()) {
|
if (!e.isDirectory()) {
|
||||||
if (p.matcher(e.getFullPath()).matches()) {
|
if (p.matcher(e.getFullPath()).matches()) {
|
||||||
return Stream.of(e);
|
return Stream.of((FileEntry) e);
|
||||||
} else {
|
} else {
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getFSTEntriesByRegExStream(e, p, allowNotInPackage);
|
return getFSTEntriesByRegExStream((DirectoryEntry) e, p, allowNotInPackage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<ContentFSTInfo> getFSTInfoForContent(FST fst, short contentIndex) {
|
public static Optional<SectionEntry> getSectionEntryForIndex(FST fst, short contentIndex) {
|
||||||
return fst.getContentFSTInfos().entrySet().stream().filter(e -> e.getKey().shortValue() == contentIndex).map(e -> e.getValue()).findAny();
|
return fst.getSectionEntries().stream().filter(e -> e.getSectionNumber() == contentIndex).findAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FSTEntry> getFSTEntriesByContentIndex(FSTEntry entry, short index) {
|
public static List<NodeEntry> getFSTEntriesByContentIndex(DirectoryEntry entry, short index) {
|
||||||
return getFSTEntriesByContentIndexAsStream(entry, index).collect(Collectors.toList());
|
return getFSTEntriesByContentIndexAsStream(entry, index).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<FSTEntry> getFSTEntriesByContentIndexAsStream(FSTEntry entry, short index) {
|
public static Stream<NodeEntry> getFSTEntriesByContentIndexAsStream(DirectoryEntry entry, short index) {
|
||||||
return entry.getChildren().stream()//
|
return entry.getChildren().stream()//
|
||||||
.flatMap(e -> {
|
.flatMap(e -> {
|
||||||
if (!e.isDir()) {
|
if (!e.isDirectory()) {
|
||||||
if (e.getContentIndex() == index) {
|
if (e.getSectionEntry().getSectionNumber() == index) {
|
||||||
return Stream.of(e);
|
return Stream.of(e);
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
return getFSTEntriesByContentIndexAsStream(e, index);
|
return getFSTEntriesByContentIndexAsStream((DirectoryEntry) e, index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does not include entries that are not in the package..
|
* Does not include entries that are not in the package..
|
||||||
*/
|
*/
|
||||||
public static Stream<FSTEntry> getAllFSTEntryChildrenAsStream(FSTEntry root) {
|
public static Stream<NodeEntry> getAllFSTEntryChildrenAsStream(DirectoryEntry root) {
|
||||||
return getAllFSTEntryChildrenAsStream(root, false);
|
return getAllFSTEntryChildrenAsStream(root, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<FSTEntry> getAllFSTEntryChildrenAsStream(FSTEntry root, boolean allowNotInPackage) {
|
public static Stream<NodeEntry> getAllFSTEntryChildrenAsStream(DirectoryEntry root, boolean allowNotInPackage) {
|
||||||
return root.getChildren().stream() //
|
return root.getChildren().stream() //
|
||||||
.filter(e -> allowNotInPackage || !e.isNotInPackage()) //
|
.filter(e -> allowNotInPackage || !e.isLink()) //
|
||||||
.flatMap(e -> {
|
.flatMap(e -> {
|
||||||
if (!e.isDir()) {
|
if (!e.isDirectory()) {
|
||||||
return Stream.of(e);
|
return Stream.of(e);
|
||||||
}
|
}
|
||||||
return getAllFSTEntryChildrenAsStream(e, allowNotInPackage);
|
return getAllFSTEntryChildrenAsStream((DirectoryEntry) e, allowNotInPackage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import java.io.InputStream;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogManager;
|
import java.util.logging.LogManager;
|
||||||
@ -266,5 +265,4 @@ public final class Utils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public abstract class AddressInBlocks<T extends BlockSize> {
|
||||||
|
private final T blockSize;
|
||||||
|
private final long value;
|
||||||
|
|
||||||
|
public long getAddressInBytes() {
|
||||||
|
return this.getValue() * this.getBlockSize().blockSize;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class AddressInDiscBlocks extends AddressInBlocks<DiscBlockSize> {
|
||||||
|
public AddressInDiscBlocks(DiscBlockSize blockSize, long value) {
|
||||||
|
super(blockSize, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AddressInDiscBlocks empty() {
|
||||||
|
return new AddressInDiscBlocks(new DiscBlockSize(0), (long) 0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class AddressInVolumeBlocks extends AddressInBlocks<VolumeBlockSize> {
|
||||||
|
public AddressInVolumeBlocks(VolumeBlockSize blockSize, long value) {
|
||||||
|
super(blockSize, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AddressInVolumeBlocks empty() {
|
||||||
|
return new AddressInVolumeBlocks(new VolumeBlockSize(0), (long) 0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public abstract class BlockSize {
|
||||||
|
@Getter protected long blockSize;
|
||||||
|
|
||||||
|
public BlockSize(long blockSize) {
|
||||||
|
this.blockSize = blockSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockSize(BlockSize copy) {
|
||||||
|
this.blockSize = copy.blockSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return Long.toString(blockSize);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class DiscBlockSize extends BlockSize {
|
||||||
|
|
||||||
|
public DiscBlockSize(long blockSize) {
|
||||||
|
super(blockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class SectionAddress extends AddressInBlocks<SectionBlockSize> {
|
||||||
|
public SectionAddress(SectionBlockSize blockSize, long value) {
|
||||||
|
super(blockSize, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SectionAddress empty() {
|
||||||
|
return new SectionAddress(new SectionBlockSize(0), (long) 0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class SectionBlockSize extends BlockSize {
|
||||||
|
|
||||||
|
public SectionBlockSize(long blockSize) {
|
||||||
|
super(blockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public abstract class SizeInBlocks<T extends BlockSize> {
|
||||||
|
private final T blockSize;
|
||||||
|
private final long value;
|
||||||
|
|
||||||
|
public long getSizeInBytes() {
|
||||||
|
return this.getValue() * this.getBlockSize().blockSize;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class SizeInVolumeBlocks extends SizeInBlocks<VolumeBlockSize> {
|
||||||
|
|
||||||
|
public SizeInVolumeBlocks(VolumeBlockSize blockSize, long value) {
|
||||||
|
super(blockSize, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package de.mas.wiiu.jnus.utils.blocksize;
|
||||||
|
|
||||||
|
public class VolumeBlockSize extends BlockSize {
|
||||||
|
public VolumeBlockSize(long blockSize) {
|
||||||
|
super(blockSize);
|
||||||
|
}
|
||||||
|
}
|
@ -73,7 +73,6 @@ public class NUSDecryption extends AESDecryption implements ContentDecryptor {
|
|||||||
return wrote;
|
return wrote;
|
||||||
}
|
}
|
||||||
if (inBlockBuffer != BLOCKSIZE) {
|
if (inBlockBuffer != BLOCKSIZE) {
|
||||||
|
|
||||||
throw new IOException("wasn't able to read " + BLOCKSIZE);
|
throw new IOException("wasn't able to read " + BLOCKSIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,8 +167,7 @@ public class NUSDecryption extends AESDecryption implements ContentDecryptor {
|
|||||||
curReadSize = (int) (toRead + writeOffset);
|
curReadSize = (int) (toRead + writeOffset);
|
||||||
}
|
}
|
||||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16));
|
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16));
|
||||||
|
if (inBlockBuffer <= 0) {
|
||||||
if (inBlockBuffer < 0) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
|
|||||||
} else {
|
} else {
|
||||||
curOffset = curOffset > HASHBLOCKSIZE ? curOffset - HASHBLOCKSIZE : 0;
|
curOffset = curOffset > HASHBLOCKSIZE ? curOffset - HASHBLOCKSIZE : 0;
|
||||||
}
|
}
|
||||||
if(curOffset < HASHEDBLOCKSIZE) {
|
if (curOffset < HASHEDBLOCKSIZE) {
|
||||||
int iv_start = (block % 16) * 20;
|
int iv_start = (block % 16) * 20;
|
||||||
IV = Arrays.copyOfRange(decryptedBlockBuffer, iv_start, iv_start + 16);
|
IV = Arrays.copyOfRange(decryptedBlockBuffer, iv_start, iv_start + 16);
|
||||||
|
|
||||||
@ -96,8 +96,8 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV,
|
public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache)
|
||||||
IVCache ivcache) throws IOException {
|
throws IOException {
|
||||||
int BLOCKSIZE = 0x08000;
|
int BLOCKSIZE = 0x08000;
|
||||||
|
|
||||||
int buffer_size = BLOCKSIZE;
|
int buffer_size = BLOCKSIZE;
|
||||||
|
@ -19,7 +19,7 @@ import org.junit.rules.TemporaryFolder;
|
|||||||
|
|
||||||
import de.mas.wiiu.jnus.NUSTitle;
|
import de.mas.wiiu.jnus.NUSTitle;
|
||||||
import de.mas.wiiu.jnus.NUSTitleLoaderLocal;
|
import de.mas.wiiu.jnus.NUSTitleLoaderLocal;
|
||||||
import de.mas.wiiu.jnus.entities.content.Content;
|
import de.mas.wiiu.jnus.entities.TMD.Content;
|
||||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||||
import de.mas.wiiu.jnus.utils.Utils;
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user