diff --git a/src/de/mas/wiiu/jnus/NUSDataProviderWumad.java b/src/de/mas/wiiu/jnus/NUSDataProviderWumad.java
new file mode 100644
index 0000000..d22cd9f
--- /dev/null
+++ b/src/de/mas/wiiu/jnus/NUSDataProviderWumad.java
@@ -0,0 +1,107 @@
+/****************************************************************************
+ * Copyright (C) 2016-2019 Maschell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ ****************************************************************************/
+package de.mas.wiiu.jnus;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+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.parser.WUDPartitionHeader;
+import de.mas.wiiu.jnus.implementations.wumad.WumadInfo;
+import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
+import de.mas.wiiu.jnus.utils.StreamUtils;
+
+public class NUSDataProviderWumad implements NUSDataProvider {
+
+ private final WumadInfo info;
+
+ private final Map files = new HashMap<>();
+
+ public NUSDataProviderWumad(WumadInfo info) {
+ this.info = info;
+ }
+
+ private Map loadFileList(ZipFile zipFile) {
+ Enumeration extends ZipEntry> zipEntries = zipFile.entries();
+ Map result = new HashMap<>();
+ while (zipEntries.hasMoreElements()) {
+ ZipEntry entry = (ZipEntry) zipEntries.nextElement();
+ if (!entry.isDirectory()) {
+ String entryName = entry.getName();
+ result.put(entryName.toLowerCase(Locale.ENGLISH), entry);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
+ if (files.isEmpty()) {
+ files.putAll(loadFileList(info.getZipFile()));
+ }
+
+ ZipEntry entry = files.values().stream().filter(e -> e.getName().startsWith("p" + info.getPartition() + "."))
+ .filter(e -> e.getName().endsWith(content.getFilename().toLowerCase())).findFirst().orElseThrow(() -> new FileNotFoundException());
+ InputStream in = info.getZipFile().getInputStream(entry);
+ StreamUtils.skipExactly(in, offset);
+
+ return in;
+ }
+
+ @Override
+ public Optional getContentH3Hash(Content content) throws IOException {
+ WUDPartitionHeader partitionHeader = info.getPartitionHeader();
+ if (!partitionHeader.isCalculatedHashes()) {
+ try {
+ partitionHeader.calculateHashes(TMD.parseTMD(getRawTMD().get()).getAllContents());
+ } catch (ParseException e) {
+ throw new IOException(e);
+ }
+ }
+ return partitionHeader.getH3Hash(content);
+ }
+
+ @Override
+ public Optional getRawTMD() throws IOException {
+ return info.getTmdData();
+ }
+
+ @Override
+ public Optional getRawTicket() throws IOException {
+ return info.getTicketData();
+ }
+
+ @Override
+ public Optional getRawCert() throws IOException {
+ return info.getCertData();
+ }
+
+ @Override
+ public void cleanup() throws IOException {
+ info.getZipFile().close();
+ }
+}
diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoaderWumad.java b/src/de/mas/wiiu/jnus/NUSTitleLoaderWumad.java
new file mode 100644
index 0000000..6da8619
--- /dev/null
+++ b/src/de/mas/wiiu/jnus/NUSTitleLoaderWumad.java
@@ -0,0 +1,46 @@
+/****************************************************************************
+ * Copyright (C) 2016-2019 Maschell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ ****************************************************************************/
+package de.mas.wiiu.jnus;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.xml.sax.SAXException;
+
+import de.mas.wiiu.jnus.implementations.wumad.WumadInfo;
+import de.mas.wiiu.jnus.implementations.wumad.WumadParser;
+
+public final class NUSTitleLoaderWumad {
+
+ private NUSTitleLoaderWumad() {
+
+ }
+
+ public static NUSTitle loadNUSTitle(File inputFile, byte[] commonKey) throws IOException, ParserConfigurationException, SAXException, ParseException {
+ NUSTitleConfig config = new NUSTitleConfig();
+
+ config.setCommonKey(commonKey);
+
+ WumadInfo wumadInfo = WumadParser.createWumadInfo(inputFile);
+
+ return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(wumadInfo));
+ }
+
+}
diff --git a/src/de/mas/wiiu/jnus/implementations/wumad/WumadInfo.java b/src/de/mas/wiiu/jnus/implementations/wumad/WumadInfo.java
new file mode 100644
index 0000000..17b66dd
--- /dev/null
+++ b/src/de/mas/wiiu/jnus/implementations/wumad/WumadInfo.java
@@ -0,0 +1,40 @@
+/****************************************************************************
+ * Copyright (C) 2016-2019 Maschell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ ****************************************************************************/
+package de.mas.wiiu.jnus.implementations.wumad;
+
+import java.util.Optional;
+import java.util.zip.ZipFile;
+
+import de.mas.wiiu.jnus.implementations.wud.parser.WUDPartitionHeader;
+import lombok.Data;
+import lombok.Setter;
+
+@Data
+public class WumadInfo {
+
+ @Setter private Optional tmdData = Optional.empty();
+ @Setter private Optional ticketData = Optional.empty();
+ @Setter private Optional certData = Optional.empty();
+
+ @Setter private ZipFile zipFile;
+ @Setter private WUDPartitionHeader partitionHeader;
+
+ @Setter private String partition;
+
+ WumadInfo() {
+ }
+}
diff --git a/src/de/mas/wiiu/jnus/implementations/wumad/WumadParser.java b/src/de/mas/wiiu/jnus/implementations/wumad/WumadParser.java
new file mode 100644
index 0000000..69fdd9d
--- /dev/null
+++ b/src/de/mas/wiiu/jnus/implementations/wumad/WumadParser.java
@@ -0,0 +1,97 @@
+/****************************************************************************
+ * Copyright (C) 2016-2019 Maschell
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ ****************************************************************************/
+package de.mas.wiiu.jnus.implementations.wumad;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+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.parser.WUDPartitionHeader;
+import de.mas.wiiu.jnus.utils.FSTUtils;
+import de.mas.wiiu.jnus.utils.StreamUtils;
+
+public class WumadParser {
+
+ 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 final String SI_FST_FILENAME = "sip.fst.00000000.app";
+ public static final String P01_HEADER = "";
+
+ public static WumadInfo createWumadInfo(File wumadFile) throws IOException, ParserConfigurationException, SAXException, ParseException {
+ WumadInfo result = new WumadInfo();
+
+ try {
+ ZipFile zipFile = new ZipFile(wumadFile);
+ result.setZipFile(zipFile);
+
+ // TODO: some .wumad doesn't have SI files.
+ ZipEntry fst = zipFile.getEntry(SI_FST_FILENAME);
+
+ byte[] fstBytes = StreamUtils.getBytesFromStream(zipFile.getInputStream(fst), (int) fst.getSize());
+
+ FST fstdd = FST.parseFST(fstBytes);
+
+ // TODO: add support for multiple partition inside a wumad
+ FSTEntry dirRoot = fstdd.getRoot().getDirChildren().get(0);
+
+ ZipEntry data = zipFile.getEntry(String.format("sip.s00%s.00000000.app", dirRoot.getFilename()));
+
+ result.setTmdData(getFSTEntryAsByte(dirRoot.getFullPath() + "/" + WUD_TMD_FILENAME, dirRoot, zipFile, data));
+ result.setCertData(getFSTEntryAsByte(dirRoot.getFullPath() + "/" + WUD_CERT_FILENAME, dirRoot, zipFile, data));
+ result.setTicketData(getFSTEntryAsByte(dirRoot.getFullPath() + "/" + WUD_TICKET_FILENAME, dirRoot, zipFile, data));
+
+ ZipEntry headerEntry = zipFile.getEntry(String.format("p%s.header.bin", dirRoot.getFilename()));
+
+ byte[] header = StreamUtils.getBytesFromStream(zipFile.getInputStream(headerEntry), (int) headerEntry.getSize());
+
+ result.setPartitionHeader(WUDPartitionHeader.parseHeader(header));
+ result.setPartition(dirRoot.getFilename());
+
+ } catch (ZipException e) {
+ e.printStackTrace();
+ throw new ParseException("Failed to parse wumad:" + e.getMessage(), 0);
+ }
+
+ return result;
+ }
+
+ private static Optional getFSTEntryAsByte(String filePath, FSTEntry dirRoot, ZipFile zipFile, ZipEntry data) throws IOException {
+ Optional entryOpt = FSTUtils.getEntryByFullPath(dirRoot, filePath);
+ if (!entryOpt.isPresent()) {
+ return Optional.empty();
+ }
+ FSTEntry entry = entryOpt.get();
+
+ InputStream in = zipFile.getInputStream(data);
+
+ StreamUtils.skipExactly(in, entry.getFileOffset());
+ return Optional.of(StreamUtils.getBytesFromStream(in, (int) entry.getFileSize()));
+ }
+}