This commit is contained in:
Maschell 2020-08-12 00:52:05 +02:00
parent 15a1df1358
commit e4d32e9910
89 changed files with 2244 additions and 1307 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/target/
.settings/
.classpath
test.1
test.2

View File

@ -25,7 +25,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
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.utils.CheckSumWrongException;
import de.mas.wiiu.jnus.utils.FSTUtils;
@ -49,7 +50,7 @@ public final class DecryptionService {
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 {
decryptFSTEntryToAsync(useFullPath, entry, outputPath, skipExistingFile).get();
} 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(() -> {
try {
if (entry.isNotInPackage()) {
if (entry.isLink()) {
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();
if (useFullPath) {
targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
if (entry.isDir()) { // If the entry is a directory. Create it and return.
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);
return;
}
} else if (entry.isDir()) {
} else if (entry.isDirectory()) {
return;
}
@ -87,16 +88,16 @@ public final class DecryptionService {
if (skipExistingFile) {
File targetFile = new File(targetFilePath);
if (targetFile.exists()) {
if (entry.isDir()) {
if (entry.isDirectory()) {
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;
} 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,25 +105,31 @@ public final class DecryptionService {
File target = new File(targetFilePath);
// 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) {
throw new CompletionException(ex);
}
});
}
public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException {
dataProvider.readFileToStream(outputStream, entry);
public void decryptFSTEntryToStream(NodeEntry entry, OutputStream outputStream) throws IOException {
if (!entry.isFile() || entry.isLink()) {
return;
}
dataProvider.readFileToStream(outputStream, (FileEntry) entry);
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Decrypt FSTEntry to OutputStream
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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));
decryptFSTEntryToStream(entry, outputStream);
if (entry.isFile()) {
decryptFSTEntryToStream(entry, outputStream);
}
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@ -132,10 +139,13 @@ public final class DecryptionService {
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles)
throws IOException, CheckSumWrongException {
FSTEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
NodeEntry entry = FSTUtils.getFSTEntryByFullPath(dataProvider.getRoot(), entryFullPath)
.orElseThrow(() -> new FileNotFoundException("File not found: " + entryFullPath));
decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
if (entry.isFile()) {
decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
}
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@ -156,11 +166,11 @@ public final class DecryptionService {
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);
}
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 {
if (parallelizable && Settings.ALLOW_PARALLELISATION) {
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 {
return CompletableFuture
.allOf(list.stream().map(entry -> decryptFSTEntryToAsync(fullPath, entry, outputFolder, skipExisting)).toArray(CompletableFuture[]::new));

View File

@ -28,7 +28,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.TMD.Content;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.interfaces.Parallelizable;
import de.mas.wiiu.jnus.utils.DataProviderUtils;

View File

@ -22,10 +22,11 @@ import java.util.List;
import java.util.Optional;
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.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.entities.FST.FST;
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.utils.FSTUtils;
import lombok.Getter;
@ -35,38 +36,38 @@ public class NUSTitle {
@Getter @Setter private Optional<FST> FST = Optional.empty();
@Getter @Setter private Optional<Ticket> ticket;
@Getter private final TMD TMD;
@Getter private final TitleMetaData TMD;
@Getter private final NUSDataProcessor dataProcessor;
private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) {
private NUSTitle(TitleMetaData tmd, NUSDataProcessor dataProcessor) {
this.TMD = tmd;
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);
result.setTicket(ticket);
result.setFST(fst);
return result;
}
public Stream<FSTEntry> getAllFSTEntriesAsStream() {
public Stream<NodeEntry> getAllFSTEntriesAsStream() {
if (!FST.isPresent()) {
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);
}
public List<FSTEntry> getFSTEntriesByRegEx(String regEx, boolean onlyInPackage) {
public List<FileEntry> getFSTEntriesByRegEx(String regEx, boolean onlyInPackage) {
if (!FST.isPresent()) {
return new ArrayList<>();
}
return FSTUtils.getFSTEntriesByRegEx(FST.get().getRoot(), regEx, onlyInPackage);
return FSTUtils.getFSTEntriesByRegEx(FST.get().getRootEntry(), regEx, onlyInPackage);
}
public void cleanup() throws IOException {

View File

@ -23,11 +23,12 @@ import java.text.ParseException;
import java.util.Optional;
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.content.Content;
import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.entities.FST.FST;
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.entities.TMD.TitleMetaData;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
@ -48,7 +49,7 @@ public class NUSTitleLoader {
throws IOException, ParseException {
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()) {
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());
FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
for (FSTEntry children : dp.getRoot().getChildren()) {
dp.readFile(children);
for (NodeEntry child : dp.getRoot().getFileChildren()) {
if (!child.isLink()) {
dp.readFile((FileEntry) child);
}
}
return result;
@ -99,7 +102,7 @@ public class NUSTitleLoader {
Content fstContent = tmd.getContentByIndex(0);
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
// on the partition.

View File

@ -19,7 +19,7 @@ package de.mas.wiiu.jnus;
import java.io.IOException;
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.NUSDataProviderFST;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
@ -33,7 +33,7 @@ public final class NUSTitleLoaderFST {
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();
config.setCommonKey(commonKey);

View File

@ -22,6 +22,7 @@ import java.text.ParseException;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
public final class NUSTitleLoaderRemote {
@ -54,7 +55,7 @@ public final class NUSTitleLoaderRemote {
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));
}
}

View File

@ -23,19 +23,23 @@ import java.nio.file.Files;
import java.text.ParseException;
import java.util.ArrayList;
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.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfoParser;
import de.mas.wiiu.jnus.implementations.wud.WiiUDisc;
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUDataPartition;
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUGMPartition;
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.utils.Utils;
import lombok.NonNull;
import lombok.val;
import lombok.var;
public final class WUDLoader {
@ -43,77 +47,88 @@ public final class WUDLoader {
super();
}
public static WUDInfo load(String WUDPath) throws IOException, ParseException {
public static WiiUDisc load(String WUDPath) throws IOException, ParseException {
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());
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);
}
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);
}
public static WUDInfo load(String WUDPath, byte[] titleKey, boolean forceNoKey) throws IOException, ParseException {
byte[] usedTitleKey = titleKey;
public static WiiUDisc load(String WUDPath, byte[] titleKey, boolean forceNoKey) throws IOException, ParseException {
Optional<byte[]> usedTitleKey = Optional.empty();
if (titleKey != null) {
usedTitleKey = Optional.of(titleKey);
}
File wudFile = new File(WUDPath);
if (!wudFile.exists()) {
throw new FileNotFoundException(wudFile.getAbsolutePath() + " was not found");
}
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);
if (!keyFile.exists()) {
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<>();
for (val gamePartition : wudInfo.getGamePartitions()) {
result.add(convertGamePartitionToNUSTitle(gamePartition, wudInfo.getWUDDiscReader(), commonKey));
List<WiiUGMPartition> gamePartitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUGMPartition)
.map(p -> (WiiUGMPartition) p).collect(Collectors.toList());
for (val gamePartition : gamePartitions) {
result.add(convertGamePartitionToNUSTitle(gamePartition, disc.getReader().get(), commonKey));
}
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 {
final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey);
gamePartition.getTmd();
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader),
(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<>();
for (val gamePartition : wudInfo.getGamePartitions()) {
NUSTitle t = convertGamePartitionToNUSTitle(gamePartition, wudInfo.getWUDDiscReader(), commonKey);
List<WiiUGMPartition> gamePartitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUGMPartition)
.map(p -> (WiiUGMPartition) p).collect(Collectors.toList());
for (val gamePartition : gamePartitions) {
NUSTitle t = convertGamePartitionToNUSTitle(gamePartition, disc.getReader().get(), commonKey);
FSTDataProviderNUSTitle res = new FSTDataProviderNUSTitle(t);
res.setName(gamePartition.getPartitionName());
res.setName(gamePartition.getVolumeID());
result.add(res);
}
for (val partition : wudInfo.getDataPartitions()) {
result.add(new FSTDataProviderWUDDataPartition(partition, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey()));
List<WiiUDataPartition> dataParitions = disc.getHeader().getContentsInformation().getPartitions().stream().filter(p -> p instanceof WiiUDataPartition)
.map(p -> ((WiiUDataPartition) p)).collect(Collectors.toList());
for (val partition : dataParitions) {
result.add(new FSTDataProviderWUDDataPartition(partition, disc.getReader().get(), disc.getDiscKey()));
}
return result;
}
}

View File

@ -41,7 +41,8 @@ public class WumadLoader {
final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey);
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 {

View File

@ -14,7 +14,7 @@
* 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;
package de.mas.wiiu.jnus.entities.TMD;
import java.nio.ByteBuffer;
import java.text.ParseException;
@ -37,7 +37,7 @@ public class Content implements Comparable<Content> {
public static final short CONTENT_FLAG_UNKWN1 = 0x4000;
public static final short CONTENT_HASHED = 0x0002;
public static final short CONTENT_ENCRYPTED = 0x0001;
public static final int CONTENT_SIZE = 0x30;
@Getter private final int ID;
@ -63,7 +63,7 @@ public class Content implements Comparable<Content> {
* @return content object
* @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) {
log.info("Error: invalid Content byte[] input");
throw new ParseException("Error: invalid Content byte[] input", 0);
@ -180,8 +180,6 @@ public class Content implements Comparable<Content> {
private long encryptedFileSize;
private byte[] SHA2Hash;
private ContentFSTInfo contentFSTInfo;
}
@Override

View File

@ -14,7 +14,7 @@
* 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;
package de.mas.wiiu.jnus.entities.TMD;
import java.nio.ByteBuffer;
import java.text.ParseException;
@ -69,7 +69,7 @@ public class ContentInfo {
* @return ContentFSTInfo object
* @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) {
log.info("Error: invalid ContentInfo byte[] input");
throw new ParseException("Error: invalid ContentInfo byte[] input", 0);

View File

@ -14,7 +14,7 @@
* 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;
package de.mas.wiiu.jnus.entities.TMD;
import java.io.File;
import java.io.IOException;
@ -27,14 +27,12 @@ import java.util.HashMap;
import java.util.List;
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.Getter;
import lombok.extern.java.Log;
@Log
public final class TMD {
public final class TitleMetaData {
private static final int SIGNATURE_LENGTH = 0x100;
private static final int ISSUER_LENGTH = 0x40;
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> contentToID = new HashMap<>();
private TMD(TMDParam param) {
private TitleMetaData(TMDParam param) {
super();
this.signatureType = param.getSignatureType();
this.signature = param.getSignature();
@ -101,7 +99,7 @@ public final class TMD {
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()) {
log.info("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()));
}
public static TMD parseTMD(byte[] input) throws ParseException {
public static TitleMetaData parseTMD(byte[] input) throws ParseException {
if (input == null || input.length == 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++) {
byte[] contentInfo = new byte[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<>();
@ -180,7 +178,7 @@ public final class TMD {
buffer.position(CONTENT_OFFSET + (Content.CONTENT_SIZE * i));
byte[] content = new byte[Content.CONTENT_SIZE];
buffer.get(content, 0, Content.CONTENT_SIZE);
Content c = Content.parseContent(content);
Content c = Content.parseData(content);
contentList.add(c);
}
@ -213,7 +211,7 @@ public final class TMD {
param.setCert1(cert1);
param.setCert2(cert2);
TMD result = new TMD(param);
TitleMetaData result = new TitleMetaData(param);
for (Content c : contentList) {
result.setContentToIndex(c.getIndex(), c);

View File

@ -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;
}
}

View File

@ -1,98 +1,206 @@
/****************************************************************************
* 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;
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.HashMap;
import java.util.Map;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
import de.mas.wiiu.jnus.utils.ByteUtils;
import lombok.Getter;
import de.mas.wiiu.jnus.entities.FST.header.Header;
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
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;
/**
* Represents the FST
*
* @author Maschell
*
*/
public final class FST {
@Getter private final FSTEntry root = FSTEntry.getRootFSTEntry();
@Data
public class FST {
private Header header;
private SectionEntries sectionEntries;
private StringTable stringTable;
private NodeEntries nodeEntries;
@Getter private final int sectorSize;
@Getter private final int contentCount;
@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) throws IOException {
return parseData(data, 0, new VolumeBlockSize(1));
}
/**
* Creates a FST by the given raw byte data
*
* @param fstData
* raw decrypted FST data
* @return
* @throws ParseException
*/
public static FST parseFST(byte[] fstData) throws ParseException {
if (!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[] { 0x46, 0x53, 0x54 })) {
throw new ParseException("Failed to parse FST", 0);
public static FST parseData(byte[] data, int offset, VolumeBlockSize blockSize) throws IOException {
FST fst = new FST();
int curOffset = offset;
fst.header = Header.parseData(Arrays.copyOfRange(data, curOffset, Header.LENGTH), 0);
curOffset += Header.LENGTH;
fst.sectionEntries = SectionEntries.parseData(data, curOffset, fst.header.getNumberOfSections(), blockSize);
curOffset += fst.sectionEntries.getSizeInBytes();
int lastEntryNumber = RootEntry.parseLastEntryNumber(data, curOffset);
fst.stringTable = StringTable.parseData(data, curOffset + (lastEntryNumber * 16), lastEntryNumber);
fst.nodeEntries = NodeEntries.parseData(data, curOffset, fst.sectionEntries, fst.stringTable, fst.header.getBlockSize());
return fst;
}
public RootEntry getRootEntry() {
return getNodeEntries().getRootEntry();
}
public byte[] getAsBytes() {
byte[] headerData = header.getAsBytes();
byte[] sectionData = sectionEntries.getAsBytes();
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();
}
public static FST createFromFolder(File folder) {
Header header = new Header();
header.setFSTVersion((short) 0);
header.setHashDisabled((short) 1);
header.setBlockSize(new SectionBlockSize(32));
VolumeBlockSize blockSize = new VolumeBlockSize(32768);
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));
}
int sectorSize = ByteUtils.getIntFromBytes(fstData, 0x04);
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
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));
}
FST result = new FST(sectorSize, contentCount);
int contentfst_offset = 0x20;
int contentfst_size = 0x20 * contentCount;
setEntryNumbers(root, 0);
int fst_offset = contentfst_offset + contentfst_size;
// root.printToStringRecursive();
int fileCount = ByteUtils.getIntFromBytes(fstData, fst_offset + 0x08);
int fst_size = fileCount * 0x10;
NodeEntries nodeEntries = new NodeEntries(root);
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;
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));
}
}
Map<Integer, ContentFSTInfo> contentFSTInfos = result.getContentFSTInfos();
for (int i = 0; i < contentCount; i++) {
byte contentFST[] = Arrays.copyOfRange(fstData, contentfst_offset + (i * 0x20), contentfst_offset + ((i + 1) * 0x20));
contentFSTInfos.put(i, ContentFSTInfo.parseContentFST(contentFST));
}
byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size);
byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize);
FSTEntry root = result.getRoot();
FSTService.parseFST(root, fstSection, nameSection, sectorSize);
return result;
}
}

View File

@ -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);
}
}

View File

@ -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)));
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 + "]";
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -9,7 +9,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
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.NUSDataProcessor;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
@ -52,10 +52,10 @@ public class DefaultNUSDataProcessor implements NUSDataProcessor {
} catch (Exception e) {
in.throwException(e);
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}).start();
@ -139,11 +139,11 @@ public class DefaultNUSDataProcessor implements NUSDataProcessor {
} catch (Exception e) {
in.throwException(e);
try {
in.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
in.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}).start();
@ -156,7 +156,7 @@ public class DefaultNUSDataProcessor implements NUSDataProcessor {
long payloadOffset = offset;
long streamOffset = payloadOffset;
long streamFilesize = 0;
streamOffset = (payloadOffset / 0xFC00) * 0x10000;
long offsetInBlock = payloadOffset - ((streamOffset / 0x10000) * 0xFC00);
if (offsetInBlock + size < 0xFC00) {
@ -182,7 +182,7 @@ public class DefaultNUSDataProcessor implements NUSDataProcessor {
throw new IOException(e);
}
} else {
InputStream in = readDecryptedContentAsStream(c, offset, size);
InputStream in = readDecryptedContentAsStream(c, offset, Utils.align(size, 16));
byte[] hash = null;
if (forceCheckHash) {

View File

@ -21,8 +21,16 @@ import java.io.OutputStream;
import java.util.stream.Collectors;
import de.mas.wiiu.jnus.NUSTitle;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
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.HasNUSTitle;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
@ -34,7 +42,7 @@ import lombok.extern.java.Log;
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
private final NUSDataProcessor dataProcessor;
private final NUSTitle title;
private final FSTEntry rootEntry;
private final RootEntry rootEntry;
@Getter @Setter private String name;
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
@ -43,36 +51,45 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
this.name = String.format("%016X", title.getTMD().getTitleID());
if (title.getFST().isPresent()) {
rootEntry = title.getFST().get().getRoot();
rootEntry = title.getFST().get().getRootEntry();
} 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.
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;
} else {
throw new IOException("No FST root entry was found");
}
}
@Override
public FSTEntry getRoot() {
public RootEntry getRoot() {
return rootEntry;
}
@Override
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
if (entry.isNotInPackage()) {
if (entry.isNotInPackage()) {
log.info("Decryption not possible because the FSTEntry is not in this package");
}
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException {
if (entry.isLink()) {
log.info("Decryption not possible because the FSTEntry is not in this package");
out.close();
return -1;
}
Content c = title.getTMD().getContentByIndex(entry.getSectionEntry().getSectionNumber());
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize());
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getOffset(), size, size == entry.getSize());
}
@Override

View File

@ -19,52 +19,53 @@ package de.mas.wiiu.jnus.implementations;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDDataPartition;
import de.mas.wiiu.jnus.entities.FST.nodeentry.FileEntry;
import de.mas.wiiu.jnus.entities.FST.nodeentry.RootEntry;
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.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.utils.FSTUtils;
public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
private final WUDDataPartition partition;
private final WiiUDataPartition partition;
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.discReader = discReader;
this.titleKey = titleKey;
this.discKey = discKey;
}
@Override
public String getName() {
return partition.getPartitionName();
return partition.getVolumeID();
}
@Override
public FSTEntry getRoot() {
return partition.getFST().getRoot();
public RootEntry getRoot() {
return partition.getFst().getRootEntry();
}
@Override
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
if (titleKey == null) {
return discReader.readEncryptedToStream(out, partition.getPartitionOffset() + info.getOffset() + entry.getFileOffset() + offset, size);
public long readFileToStream(OutputStream out, FileEntry entry, long offset, long size) throws IOException {
SectionEntry info = entry.getSectionEntry();
if (!discKey.isPresent()) {
return discReader.readEncryptedToStream(out,
partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes() + entry.getOffset() + offset, size);
}
return discReader.readDecryptedToOutputStream(out, partition.getPartitionOffset() + info.getOffset(), entry.getFileOffset() + offset, size, titleKey,
null, false);
return discReader.readDecryptedToOutputStream(out, partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes(),
entry.getOffset() + offset, size, discKey.get(), null, false);
}
@Override
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
if (titleKey == null) {
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
return discReader.readEncryptedToStream(partition.getPartitionOffset() + info.getOffset() + entry.getFileOffset() + offset, size);
public InputStream readFileAsStream(FileEntry entry, long offset, long size) throws IOException {
if (!discKey.isPresent()) {
SectionEntry info = entry.getSectionEntry();
return discReader.readEncryptedToStream(
partition.getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes() + entry.getOffset() + offset, size);
}
return FSTDataProvider.super.readFileAsStream(entry, offset, size);

View File

@ -7,7 +7,8 @@ import java.io.OutputStream;
import java.util.zip.ZipEntry;
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.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.utils.StreamUtils;
@ -27,24 +28,23 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider {
}
@Override
public FSTEntry getRoot() {
return dataPartition.getFST().getRoot();
public RootEntry getRoot() {
return dataPartition.getFST().getRootEntry();
}
@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);
}
@Override
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
ZipEntry zipEntry = zipFile.stream()
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex())))
.findFirst()
.orElseThrow(() -> new FileNotFoundException());
public InputStream readFileAsStream(FileEntry entry, long offset, long size) throws IOException {
ZipEntry zipEntry = zipFile.stream().filter(
e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getSectionEntry().getSectionNumber())))
.findFirst().orElseThrow(() -> new FileNotFoundException());
InputStream in = zipFile.getInputStream(zipEntry);
StreamUtils.skipExactly(in, offset + entry.getFileOffset());
StreamUtils.skipExactly(in, offset + entry.getOffset());
return in;
}

View File

@ -24,8 +24,10 @@ import java.util.Map;
import java.util.Optional;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
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.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
@ -33,9 +35,9 @@ import de.mas.wiiu.jnus.utils.FSTUtils;
public class NUSDataProviderFST implements NUSDataProvider {
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.fstDataProvider = fstDataProvider;
}
@ -47,9 +49,12 @@ public class NUSDataProviderFST implements NUSDataProvider {
@Override
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
String filename = content.getFilename();
Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));
return fstDataProvider.readFileAsStream(contentFile, offset, size);
Optional<NodeEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
NodeEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));
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<>();
@ -64,12 +69,14 @@ public class NUSDataProviderFST implements NUSDataProvider {
return res;
}
private Optional<byte[]> readFileByFilename(FSTEntry base, String filename) throws IOException {
Optional<FSTEntry> entryOpt = FSTUtils.getChildOfDirectory(base, filename);
private Optional<byte[]> readFileByFilename(DirectoryEntry base, String filename) throws IOException {
Optional<NodeEntry> entryOpt = FSTUtils.getChildOfDirectory(base, filename);
if (entryOpt.isPresent()) {
FSTEntry entry = entryOpt.get();
return Optional.of(fstDataProvider.readFile(entry));
NodeEntry entry = entryOpt.get();
if (!entry.isFile() || entry.isLink()) {
return Optional.empty();
}
return Optional.of(fstDataProvider.readFile((FileEntry) entry));
}
return Optional.empty();
}

View File

@ -25,7 +25,7 @@ import java.nio.file.Files;
import java.util.Optional;
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.utils.FileUtils;
import de.mas.wiiu.jnus.utils.StreamUtils;

View File

@ -25,7 +25,7 @@ import java.nio.file.Files;
import java.util.Optional;
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.utils.StreamUtils;
import lombok.Getter;
@ -40,7 +40,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
public NUSDataProviderLocalBackup(String localPath, short version) {
this.localPath = localPath;
this.titleVersion = version;
this.titleVersion = version;
}
private String getFilePathOnDisk(Content c) {

View File

@ -23,7 +23,7 @@ import java.util.Map;
import java.util.Optional;
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.Parallelizable;
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
@ -32,15 +32,16 @@ import lombok.Getter;
public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
@Getter private final int version;
@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.titleID = titleID;
this.downloadService = downloadService;
}
@Override
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
}
@ -68,13 +69,11 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
return resOpt;
}
Optional<byte[]> tmdCache = null;
Optional<byte[]> tmdCache = Optional.empty();
@Override
public Optional<byte[]> getRawTMD() throws IOException {
if (tmdCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
if (!tmdCache.isPresent()) {
long titleID = getTitleID();
int version = getVersion();
@ -88,17 +87,16 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
return tmdCache;
}
Optional<byte[]> ticketCache = null;
Optional<byte[]> ticketCache = Optional.empty();
@Override
public Optional<byte[]> getRawTicket() throws IOException {
if (ticketCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
if (!ticketCache.isPresent()) {
byte[] res = downloadService.downloadTicketToByteArray(titleID);
if (res == null || res.length == 0) {
return Optional.empty();
}
ticketCache = Optional.of(res);
ticketCache = Optional.of(res);
}
return ticketCache;
}

View File

@ -18,41 +18,54 @@ package de.mas.wiiu.jnus.implementations;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
import de.mas.wiiu.jnus.entities.FST.FST;
import de.mas.wiiu.jnus.entities.FST.sectionentry.SectionEntry;
import de.mas.wiiu.jnus.entities.TMD.Content;
import de.mas.wiiu.jnus.implementations.wud.content.partitions.WiiUGMPartition;
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
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.var;
import lombok.extern.java.Log;
@Log
public class NUSDataProviderWUD implements NUSDataProvider {
@Getter private final WUDGamePartition gamePartition;
@Getter private final WiiUGMPartition gamePartition;
@Getter private final WUDDiscReader discReader;
@Getter private FST fst;
public NUSDataProviderWUD(WUDGamePartition gamePartition, WUDDiscReader discReader) {
public NUSDataProviderWUD(WiiUGMPartition gamePartition, WUDDiscReader discReader) {
this.gamePartition = gamePartition;
this.discReader = discReader;
}
@Override
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;
}
public long getOffsetInWUD(Content content) throws IOException {
if (content.getIndex() == 0) { // Index 0 is the FST which is at the beginning of the partion;
return getGamePartition().getPartitionOffset();
if (content.getIndex() == 0) { // Index 0 is the FST which is at the beginning of the partition;
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"));
return getGamePartition().getPartitionOffset() + info.getOffset();
SectionEntry info = FSTUtils.getSectionEntryForIndex(fst, content.getIndex()).orElseThrow(() -> new IOException("Failed to find FSTInfo"));
return getGamePartition().getSectionOffsetOnDefaultPartition() + info.getAddress().getAddressInBytes();
}
@Override
@ -60,16 +73,22 @@ public class NUSDataProviderWUD implements NUSDataProvider {
WUDDiscReader discReader = getDiscReader();
long offset = getOffsetInWUD(content) + fileOffsetBlock;
return discReader.readEncryptedToStream(offset, size);
return discReader.readEncryptedToStream(offset, Utils.align(size, 16));
}
@Override
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
if (!getGamePartitionHeader().isCalculatedHashes()) {
log.info("Calculating h3 hashes");
getGamePartitionHeader().calculateHashes(getGamePartition().getTmd().getAllContents());
byte[] hash = getGamePartition().getVolumes().values().iterator().next().getH3HashArrayList().get(content.getIndex()).getH3HashArray();
// Checking the hash of the h3 file.
try {
if (!Arrays.equals(HashUtil.hashSHA1(hash), content.getSHA2Hash())) {
log.warning("h3 incorrect from WUD");
}
} catch (NoSuchAlgorithmException e) {
log.warning(e.getMessage());
}
return getGamePartitionHeader().getH3Hash(content);
return Optional.of(hash);
}
@Override
@ -87,10 +106,6 @@ public class NUSDataProviderWUD implements NUSDataProvider {
return Optional.of(getGamePartition().getRawCert());
}
public GamePartitionHeader getGamePartitionHeader() {
return getGamePartition().getPartitionHeader();
}
@Override
public void cleanup() {
// We don't need it

View File

@ -24,7 +24,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
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.WoomyZipFile;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;

View File

@ -19,7 +19,8 @@ package de.mas.wiiu.jnus.implementations;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
@ -28,13 +29,14 @@ import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
import de.mas.wiiu.jnus.entities.TMD.Content;
import de.mas.wiiu.jnus.implementations.wud.wumad.WumadGamePartition;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.HashUtil;
import de.mas.wiiu.jnus.utils.StreamUtils;
import lombok.extern.java.Log;
@Log
public class NUSDataProviderWumad implements NUSDataProvider {
private final ZipFile wumad;
private final WumadGamePartition partition;
@ -72,15 +74,17 @@ public class NUSDataProviderWumad implements NUSDataProvider {
@Override
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
GamePartitionHeader partitionHeader = partition.getPartitionHeader();
if (!partitionHeader.isCalculatedHashes()) {
try {
partitionHeader.calculateHashes(TMD.parseTMD(getRawTMD().get()).getAllContents());
} catch (ParseException e) {
throw new IOException(e);
byte[] hash = partition.getPartitionHeader().getH3HashArrayList().get(content.getIndex()).getH3HashArray();
// Checking the hash of the h3 file.
try {
if (!Arrays.equals(HashUtil.hashSHA1(hash), content.getSHA2Hash())) {
log.warning("h3 incorrect from WUD");
}
} catch (NoSuchAlgorithmException e) {
log.warning(e.getMessage());
}
return partitionHeader.getH3Hash(content);
return Optional.of(hash);
}
@Override

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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) + "]";
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -72,8 +72,7 @@ public abstract class WUDDiscReader {
return in;
}
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV,
boolean useFixedIV) throws IOException {
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV, boolean useFixedIV) throws IOException {
PipedOutputStream out = new PipedOutputStream();
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000);

View File

@ -1,10 +1,10 @@
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;
public class WumadDataPartition extends WumadPartition {
@Getter private final FST FST;
public WumadDataPartition(String partitionName, FST fst) {

View File

@ -2,27 +2,26 @@ package de.mas.wiiu.jnus.implementations.wud.wumad;
import java.text.ParseException;
import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.implementations.wud.GamePartitionHeader;
import de.mas.wiiu.jnus.entities.TMD.TitleMetaData;
import de.mas.wiiu.jnus.implementations.wud.content.partitions.volumes.VolumeHeader;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
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[] rawCert;
private final byte[] rawTicket;
public WumadGamePartition(String partitionName, GamePartitionHeader partitionHeader, byte[] rawTMD, byte[] rawCert, byte[] rawTicket)
throws ParseException {
public WumadGamePartition(String partitionName, VolumeHeader partitionHeader, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) throws ParseException {
super(partitionName);
this.partitionHeader = partitionHeader;
this.rawTMD = rawTMD;
this.tmd = TMD.parseTMD(rawTMD);
this.tmd = TitleMetaData.parseTMD(rawTMD);
this.rawCert = rawCert;
this.rawTicket = rawTicket;
}

View File

@ -34,9 +34,10 @@ import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
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.entities.FST.FST;
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
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.StreamUtils;
import lombok.val;
@ -61,15 +62,15 @@ public class WumadParser {
Map<String, List<ZipEntry>> allPartitions = zipFile.stream().filter(e -> e.getName().startsWith("p"))
.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.
ZipEntry siFST = zipFile.getEntry(SI_FST_FILENAME);
if (siFST != null) {
byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(siFST), (int) siFST.getSize());
FST parsedFST = FST.parseFST(fstBytes);
gamepartitions.putAll(parsedFST.getRoot().getDirChildren().stream().collect(Collectors.toMap(e -> e.getFilename(), e -> e)));
FST parsedFST = FST.parseData(fstBytes);
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.
@ -87,7 +88,7 @@ public class WumadParser {
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);
allPartitions.remove(e.getKey());
@ -99,7 +100,7 @@ public class WumadParser {
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);
@ -115,16 +116,16 @@ public class WumadParser {
return result;
}
private static Optional<byte[]> getFSTEntryAsByte(String filePath, FSTEntry dirRoot, ZipFile zipFile, ZipEntry data) throws IOException {
Optional<FSTEntry> entryOpt = FSTUtils.getEntryByFullPath(dirRoot, filePath);
private static Optional<byte[]> getFSTEntryAsByte(String filePath, DirectoryEntry dirRoot, ZipFile zipFile, ZipEntry data) throws IOException {
Optional<FileEntry> entryOpt = FSTUtils.getEntryByFullPath(dirRoot, filePath);
if (!entryOpt.isPresent()) {
return Optional.empty();
}
FSTEntry entry = entryOpt.get();
FileEntry entry = entryOpt.get();
InputStream in = zipFile.getInputStream(data);
StreamUtils.skipExactly(in, entry.getFileOffset());
return Optional.of(StreamUtils.getBytesFromStream(in, (int) entry.getFileSize()));
StreamUtils.skipExactly(in, entry.getOffset());
return Optional.of(StreamUtils.getBytesFromStream(in, (int) entry.getSize()));
}
}

View File

@ -8,13 +8,19 @@ public interface ContentDecryptor {
/**
*
* @param in InputStream of the Encrypted Data with hashed
* @param out OutputStream of the decrypted data with hashes
* @param offset 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
* @param in
* InputStream of the Encrypted Data with hashed
* @param out
* OutputStream of the decrypted data with hashes
* @param offset
* 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
* @throws IOException
*/
long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes) throws IOException;

View File

@ -10,6 +10,7 @@ public interface ContentEncryptor {
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;
}

View File

@ -22,19 +22,20 @@ import java.io.InputStream;
import java.io.OutputStream;
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;
public interface FSTDataProvider {
public String getName();
public FSTEntry getRoot();
public RootEntry getRoot();
default public byte[] readFile(FSTEntry entry) throws IOException {
return readFile(entry, 0, entry.getFileSize());
default public byte[] readFile(FileEntry entry) throws IOException {
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();
readFileToStream(out, entry, offset, size);
@ -42,11 +43,11 @@ public interface FSTDataProvider {
return out.toByteArray();
}
default public InputStream readFileAsStream(FSTEntry entry) throws IOException {
return readFileAsStream(entry, 0, entry.getFileSize());
default public InputStream readFileAsStream(FileEntry entry) throws IOException {
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();
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000);
@ -57,24 +58,24 @@ public interface FSTDataProvider {
} catch (Exception e) {
in.throwException(e);
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}).start();
return in;
}
default public long readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
return readFileToStream(out, entry, 0, entry.getFileSize());
default public long readFileToStream(OutputStream out, FileEntry entry) throws IOException {
return readFileToStream(out, entry, 0, entry.getSize());
}
default public long readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
return readFileToStream(out, entry, offset, entry.getFileSize());
default public long readFileToStream(OutputStream out, FileEntry entry, long offset) throws IOException {
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;
}

View File

@ -5,7 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
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;
public interface NUSDataProcessor {
@ -50,7 +50,7 @@ public interface NUSDataProcessor {
default public byte[] readDecryptedContent(Content c, long offset, long size) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
long len = readDecryptedContentToStream(out, c, offset, size);
if(len < 0) {
if (len < 0) {
return new byte[0];
}
return out.toByteArray();
@ -71,7 +71,7 @@ public interface NUSDataProcessor {
}
default public long readDecryptedContentToStream(OutputStream out, Content c, long offset, long size) throws IOException {
InputStream in = readDecryptedContentAsStream(c, offset, size);
InputStream in = readDecryptedContentAsStream(c, offset, size);
return StreamUtils.saveInputStreamToOutputStream(in, out, size);
}
@ -83,7 +83,7 @@ public interface NUSDataProcessor {
ByteArrayOutputStream out = new ByteArrayOutputStream();
long len = readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash);
if(len < 0) {
if (len < 0) {
return new byte[0];
}

View File

@ -21,8 +21,8 @@ import java.io.IOException;
import java.io.InputStream;
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;
public interface NUSDataProvider {

View File

@ -3,14 +3,12 @@ package de.mas.wiiu.jnus.interfaces;
import java.util.Objects;
import java.util.function.Function;
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(
Function<? super R, ? extends V> after) {
default <V> TriFunction<A, B, C, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (A a, B b, C c) -> after.apply(apply(a, b, c));
}

View File

@ -99,7 +99,7 @@ public final class ByteUtils {
ByteBuffer.allocate(2).putShort(value).get(result);
return result;
}
public static short getByteFromBytes(byte[] input, int offset) {
ByteBuffer buffer = ByteBuffer.allocate(2).put(Arrays.copyOfRange(input, offset, offset + 1)).order(ByteOrder.BIG_ENDIAN);
buffer.position(0);

View File

@ -24,7 +24,7 @@ import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.TMD.Content;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import lombok.NonNull;
import lombok.extern.java.Log;

View File

@ -25,20 +25,22 @@ import java.util.stream.Stream;
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.FSTEntry;
import de.mas.wiiu.jnus.entities.FST.FST;
import de.mas.wiiu.jnus.entities.FST.nodeentry.DirectoryEntry;
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;
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, "/");
if (!fullPath.startsWith("/")) {
fullPath = "/" + fullPath;
}
String dirPath = FilenameUtils.getFullPathNoEndSeparator(fullPath);
Optional<FSTEntry> pathOpt = Optional.of(root);
Optional<DirectoryEntry> pathOpt = Optional.of(root);
if (!dirPath.equals("/")) {
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());
}
public static Optional<FSTEntry> getFileEntryDir(FSTEntry curEntry, String string) {
public static Optional<DirectoryEntry> getFileEntryDir(DirectoryEntry curEntry, String string) {
string = string.replace(File.separator, "/");
// 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();
}
public static Optional<FSTEntry> getEntryByFullPath(FSTEntry root, String filePath) {
for (FSTEntry cur : root.getFileChildren()) {
public static Optional<FileEntry> getEntryByFullPath(DirectoryEntry root, String filePath) {
for (FileEntry cur : root.getFileChildren()) {
if (cur.getFullPath().equals(filePath)) {
return Optional.of(cur);
}
}
for (FSTEntry cur : root.getDirChildren()) {
Optional<FSTEntry> res = getEntryByFullPath(cur, filePath);
for (DirectoryEntry cur : root.getDirChildren()) {
Optional<FileEntry> res = getEntryByFullPath(cur, filePath);
if (res.isPresent()) {
return res;
}
@ -87,75 +89,75 @@ public class FSTUtils {
return Optional.empty();
}
public static Optional<FSTEntry> getChildOfDirectory(FSTEntry root, String filename) {
for (FSTEntry cur : root.getChildren()) {
if (cur.getFilename().equalsIgnoreCase(filename)) {
public static Optional<NodeEntry> getChildOfDirectory(DirectoryEntry root, String filename) {
for (NodeEntry cur : root.getChildren()) {
if (cur.getName().equalsIgnoreCase(filename)) {
return Optional.of(cur);
}
}
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);
}
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);
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()//
.filter(e -> allowNotInPackage || !e.isNotInPackage()) //
.filter(e -> allowNotInPackage || !e.isLink()) //
.flatMap(e -> {
if (!e.isDir()) {
if (!e.isDirectory()) {
if (p.matcher(e.getFullPath()).matches()) {
return Stream.of(e);
return Stream.of((FileEntry) e);
} else {
return Stream.empty();
}
}
return getFSTEntriesByRegExStream(e, p, allowNotInPackage);
return getFSTEntriesByRegExStream((DirectoryEntry) e, p, allowNotInPackage);
});
}
public static Optional<ContentFSTInfo> getFSTInfoForContent(FST fst, short contentIndex) {
return fst.getContentFSTInfos().entrySet().stream().filter(e -> e.getKey().shortValue() == contentIndex).map(e -> e.getValue()).findAny();
public static Optional<SectionEntry> getSectionEntryForIndex(FST fst, short contentIndex) {
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());
}
public static Stream<FSTEntry> getFSTEntriesByContentIndexAsStream(FSTEntry entry, short index) {
public static Stream<NodeEntry> getFSTEntriesByContentIndexAsStream(DirectoryEntry entry, short index) {
return entry.getChildren().stream()//
.flatMap(e -> {
if (!e.isDir()) {
if (e.getContentIndex() == index) {
if (!e.isDirectory()) {
if (e.getSectionEntry().getSectionNumber() == index) {
return Stream.of(e);
}
return Stream.empty();
}
return getFSTEntriesByContentIndexAsStream(e, index);
return getFSTEntriesByContentIndexAsStream((DirectoryEntry) e, index);
});
}
/**
* 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);
}
public static Stream<FSTEntry> getAllFSTEntryChildrenAsStream(FSTEntry root, boolean allowNotInPackage) {
public static Stream<NodeEntry> getAllFSTEntryChildrenAsStream(DirectoryEntry root, boolean allowNotInPackage) {
return root.getChildren().stream() //
.filter(e -> allowNotInPackage || !e.isNotInPackage()) //
.filter(e -> allowNotInPackage || !e.isLink()) //
.flatMap(e -> {
if (!e.isDir()) {
if (!e.isDirectory()) {
return Stream.of(e);
}
return getAllFSTEntryChildrenAsStream(e, allowNotInPackage);
return getAllFSTEntryChildrenAsStream((DirectoryEntry) e, allowNotInPackage);
});
}
}

View File

@ -23,7 +23,6 @@ import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.LogManager;
@ -266,5 +265,4 @@ public final class Utils {
return null;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package de.mas.wiiu.jnus.utils.blocksize;
public class DiscBlockSize extends BlockSize {
public DiscBlockSize(long blockSize) {
super(blockSize);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package de.mas.wiiu.jnus.utils.blocksize;
public class SectionBlockSize extends BlockSize {
public SectionBlockSize(long blockSize) {
super(blockSize);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package de.mas.wiiu.jnus.utils.blocksize;
public class VolumeBlockSize extends BlockSize {
public VolumeBlockSize(long blockSize) {
super(blockSize);
}
}

View File

@ -37,7 +37,7 @@ public class AESEncryption {
@Getter @Setter private byte[] AESKey;
@Getter @Setter private byte[] IV;
public AESEncryption(byte[] AESKey, byte[] IV) throws NoSuchProviderException {
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");

View File

@ -73,7 +73,6 @@ public class NUSDecryption extends AESDecryption implements ContentDecryptor {
return wrote;
}
if (inBlockBuffer != 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);
}
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16));
if (inBlockBuffer < 0) {
if (inBlockBuffer <= 0) {
break;
}

View File

@ -35,7 +35,7 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
}
return encrypt(blockBuffer, offset, BLOCKSIZE);
}
@Override
public long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException {
int BLOCKSIZE = 0x10000;
@ -61,28 +61,28 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
if (inBlockBuffer != buffer_size) {
break;
}
long curOffset = Math.max(0, payloadOffset - offset - read);
byte[] IV = new byte[16];
if (curOffset < HASHBLOCKSIZE) {
byte[] encryptedhashes = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, 0, HASHBLOCKSIZE), HASHBLOCKSIZE, IV);
long writeLength = Math.min((encryptedhashes.length - curOffset), (size - written));
out.write(encryptedhashes, (int) curOffset, (int) writeLength);
written += writeLength;
} else {
curOffset = curOffset > HASHBLOCKSIZE ? curOffset - HASHBLOCKSIZE : 0;
}
if(curOffset < HASHEDBLOCKSIZE) {
if (curOffset < HASHEDBLOCKSIZE) {
int iv_start = (block % 16) * 20;
IV = Arrays.copyOfRange(decryptedBlockBuffer, iv_start, iv_start + 16);
byte[] encryptedContent = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, HASHBLOCKSIZE, HASHEDBLOCKSIZE + HASHBLOCKSIZE),
HASHEDBLOCKSIZE, IV);
long writeLength = Math.min((encryptedContent.length - curOffset), (size - written));
long writeLength = Math.min((encryptedContent.length - curOffset), (size - written));
out.write(encryptedContent, (int) curOffset, (int) writeLength);
written += writeLength;
}
@ -96,8 +96,8 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
}
@Override
public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV,
IVCache ivcache) throws IOException {
public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache)
throws IOException {
int BLOCKSIZE = 0x08000;
int buffer_size = BLOCKSIZE;
@ -140,7 +140,7 @@ public class NUSEncryption extends AESEncryption implements ContentEncryptor {
long writeOffset = Math.max(0, payloadOffset - offset - read);
long writeLength = Math.min((output.length - writeOffset), (size - written));
out.write(output, (int) writeOffset, (int) writeLength);
written += writeLength;
} while (written < size);

View File

@ -19,7 +19,7 @@ import org.junit.rules.TemporaryFolder;
import de.mas.wiiu.jnus.NUSTitle;
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.utils.HashUtil;
import de.mas.wiiu.jnus.utils.Utils;