From 07430cc2c795b57a41ea24a38b0a7e88e919a215 Mon Sep 17 00:00:00 2001 From: Maschell Date: Sun, 20 Dec 2020 15:07:53 +0100 Subject: [PATCH] Add support for reading wudmad archive files --- .../java/de/mas/wiiu/jnus/WUDService.java | 2 +- .../jnus/implementations/wud/WUDImage.java | 19 +- .../wud/reader/WUDDiscReaderWumadArchive.java | 176 ++++++++++++++++++ .../wud/reader/WUMADAOffsetInfo.java | 11 ++ 4 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderWumadArchive.java create mode 100644 src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUMADAOffsetInfo.java diff --git a/src/main/java/de/mas/wiiu/jnus/WUDService.java b/src/main/java/de/mas/wiiu/jnus/WUDService.java index 5535e49..ba50a1c 100644 --- a/src/main/java/de/mas/wiiu/jnus/WUDService.java +++ b/src/main/java/de/mas/wiiu/jnus/WUDService.java @@ -102,7 +102,7 @@ public final class WUDService { WUDImageCompressedInfo info = new WUDImageCompressedInfo(WUDImageCompressedInfo.SECTOR_SIZE, 0, toReadFilesize); byte[] header = info.getHeaderAsBytes(); - log.info("Writing header + " + header.length); + log.info("Writing header"); fileOutput.write(header); int sectorTableEntryCount = (int) ((toReadFilesize + WUDImageCompressedInfo.SECTOR_SIZE - 1) / (long) WUDImageCompressedInfo.SECTOR_SIZE); diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/WUDImage.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/WUDImage.java index fc5a872..093f157 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/wud/WUDImage.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/WUDImage.java @@ -27,6 +27,7 @@ import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader; import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderCompressed; import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderSplitted; import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderUncompressed; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderWumadArchive; import de.mas.wiiu.jnus.utils.ByteUtils; import lombok.Getter; import lombok.Setter; @@ -41,6 +42,7 @@ public class WUDImage { @Getter private final boolean isCompressed; @Getter private final boolean isSplitted; + @Getter private final boolean isWUMADArchiveFormat; private long inputFileSize = 0L; @Getter private final WUDDiscReader WUDDiscReader; @@ -57,6 +59,9 @@ public class WUDImage { RandomAccessFile fileStream = new RandomAccessFile(file, "r"); fileStream.seek(0); + + this.fileHandle = file; + byte[] wuxheader = new byte[WUDImageCompressedInfo.WUX_HEADER_SIZE]; fileStream.read(wuxheader); WUDImageCompressedInfo compressedInfo = new WUDImageCompressedInfo(wuxheader); @@ -65,6 +70,7 @@ public class WUDImage { log.fine("Image is compressed"); this.isCompressed = true; this.isSplitted = false; + this.isWUMADArchiveFormat = false; Map indexTable = new HashMap<>(); long offsetIndexTable = compressedInfo.getOffsetIndexTable(); fileStream.seek(offsetIndexTable); @@ -80,6 +86,12 @@ public class WUDImage { setCompressedInfo(compressedInfo); } else { this.isCompressed = false; + int magic0 = ByteUtils.getIntFromBytes(wuxheader, 0x00, ByteOrder.BIG_ENDIAN); + if (magic0 == 0x57696920) { + this.isWUMADArchiveFormat = true; + }else { + this.isWUMADArchiveFormat = false; + } if (file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) && (file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)) { this.isSplitted = true; @@ -93,12 +105,13 @@ public class WUDImage { this.WUDDiscReader = new WUDDiscReaderCompressed(this, readOffset); } else if (isSplitted()) { this.WUDDiscReader = new WUDDiscReaderSplitted(this, readOffset); + } else if ((isWUMADArchiveFormat())) { + this.WUDDiscReader = new WUDDiscReaderWumadArchive(this, readOffset); } else { this.WUDDiscReader = new WUDDiscReaderUncompressed(this, readOffset); } - fileStream.close(); - this.fileHandle = file; + } public long getWUDFileSize() { @@ -107,6 +120,8 @@ public class WUDImage { inputFileSize = calculateSplittedFileSize(); } else if (isCompressed()) { inputFileSize = getCompressedInfo().getUncompressedSize(); + } else if (isWUMADArchiveFormat()){ + inputFileSize = WUDImage.WUD_FILESIZE; } else { inputFileSize = getFileHandle().length(); } diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderWumadArchive.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderWumadArchive.java new file mode 100644 index 0000000..9c792b3 --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderWumadArchive.java @@ -0,0 +1,176 @@ +/**************************************************************************** + * 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.wud.reader; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TreeMap; + +import de.mas.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.utils.ByteUtils; +import de.mas.wiiu.jnus.utils.StreamUtils; +import lombok.val; + +public class WUDDiscReaderWumadArchive extends WUDDiscReader { + private final Map offsetMap = new TreeMap<>(); + private final int WUMADA_HEADER_SIZE = 0x10000; + + public WUDDiscReaderWumadArchive(WUDImage image, long baseOffset) throws IOException { + super(image, baseOffset); + + FileInputStream input = new FileInputStream(getImage().getFileHandle()); + StreamUtils.skipExactly(input, 0 + this.getBaseOffset()); + + byte[] rawHeader = StreamUtils.getBytesFromStream(input, WUMADA_HEADER_SIZE); + + int fileTableOffset = 0x1000; + int sizeSectorSize = 0x8000; + int offsetSectorSize = 0x800; + + long curOffsetInFile = fileTableOffset; + long curOffset = WUMADA_HEADER_SIZE; + + // Mapping. + while (true) { + long curValue = ByteUtils.getUnsingedIntFromBytes(rawHeader, (int) curOffsetInFile, ByteOrder.LITTLE_ENDIAN); + if (curValue == 0 && curOffsetInFile != fileTableOffset) { + break; + } + curOffsetInFile += 4; + long size = ByteUtils.getUnsingedIntFromBytes(rawHeader, (int) curOffsetInFile, ByteOrder.LITTLE_ENDIAN); + curOffsetInFile += 4; + + offsetMap.put(curValue * offsetSectorSize, new WUMADAOffsetInfo(curOffset, size * sizeSectorSize, curValue * offsetSectorSize, false)); + curOffset += size * sizeSectorSize; + } + + // Fill in empty regions + long calculatedOffset = 0; + Map offsetMapEmptyRegions = new TreeMap<>(); + long lastEntry = 0; + for (val curEntry : offsetMap.entrySet()) { + long offsetOnDisc = curEntry.getKey(); + if (offsetOnDisc != calculatedOffset) { + offsetMapEmptyRegions.put(calculatedOffset, new WUMADAOffsetInfo(0, offsetOnDisc - calculatedOffset, calculatedOffset, true)); + calculatedOffset += offsetOnDisc - calculatedOffset; + } + calculatedOffset += curEntry.getValue().getSize(); + lastEntry = calculatedOffset; + } + if (lastEntry < WUDImage.WUD_FILESIZE) { + offsetMap.put(lastEntry, new WUMADAOffsetInfo(0, WUDImage.WUD_FILESIZE - lastEntry, lastEntry, true)); + } + offsetMap.putAll(offsetMapEmptyRegions); + } + + @Override + public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException { + FileInputStream input = new FileInputStream(getImage().getFileHandle()); + + WUMADAOffsetInfo curOffsetInfo = getOffsetInfoForOffset(offset).orElseThrow(() -> new IOException("Invalid read")); + + long targetOffsetOfSectionStart = curOffsetInfo.getTargetOffset(); + long inSectionOffset = offset - targetOffsetOfSectionStart; + + if (inSectionOffset > curOffsetInfo.getSize()) { + throw new IOException("offset > size"); + } + + long realOffset = inSectionOffset + curOffsetInfo.getOffset(); + + StreamUtils.skipExactly(input, realOffset + this.getBaseOffset()); + + int bufferSize = 0x8000; + byte[] buffer = new byte[bufferSize]; + long totalread = 0; + long readInSection = 0; + long maximumToRead = curOffsetInfo.getSize() - inSectionOffset; + do { + int read = 0; + if (curOffsetInfo.isEmptyContent()) { + Arrays.fill(buffer, (byte) 0); + read = bufferSize; + } else { + read = input.read(buffer); + if (read < 0) { + break; + } + } + readInSection += read; + // System.out.println(readInSection + " read in section " + maximumToRead); + + if (readInSection >= maximumToRead) { + + read -= (int) (readInSection - maximumToRead); + long offsetRead = offset + totalread + read; + if (offsetRead < offset + size) { + curOffsetInfo = getOffsetInfoForOffset(offsetRead).orElseThrow(() -> new IOException("Invalid read for offset " + offsetRead)); + maximumToRead = curOffsetInfo.getSize(); + readInSection = 0; + + input.close(); + input = new FileInputStream(getImage().getFileHandle()); + StreamUtils.skipExactly(input, curOffsetInfo.getOffset() + this.getBaseOffset()); + } + + } + + if (totalread + read > size) { + read = (int) (size - totalread); + } + try { + outputStream.write(Arrays.copyOfRange(buffer, 0, read)); + } catch (IOException e) { + if (e.getMessage().equals("Pipe closed")) { + break; + } else { + input.close(); + throw e; + } + } + totalread += read; + } while (totalread < size); + input.close(); + outputStream.close(); + return totalread; + } + + private Optional getOffsetInfoForOffset(long offset) { + Optional curOffsetInfo = Optional.empty(); + for (Entry test : offsetMap.entrySet()) { + val start = test.getValue().getTargetOffset(); + val end = start + test.getValue().getSize(); + + if (offset < start) { + continue; + } + + if (offset < end) { + curOffsetInfo = Optional.of(test.getValue()); + break; + } + } + return curOffsetInfo; + } + +} diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUMADAOffsetInfo.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUMADAOffsetInfo.java new file mode 100644 index 0000000..827ce41 --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUMADAOffsetInfo.java @@ -0,0 +1,11 @@ +package de.mas.wiiu.jnus.implementations.wud.reader; + +import lombok.Data; + +@Data +public class WUMADAOffsetInfo { + private final long offset; + private final long size; + private final long targetOffset; + private final boolean emptyContent; +}