332 lines
13 KiB
Java
332 lines
13 KiB
Java
/*
|
|
Copyright 2019-2022 Dmitry Isaenko
|
|
|
|
This file is part of libKonogonka.
|
|
|
|
libKonogonka 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.
|
|
|
|
libKonogonka 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 libKonogonka. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
package libKonogonka.Tools.PFS0;
|
|
|
|
import libKonogonka.Converter;
|
|
import libKonogonka.RainbowDump;
|
|
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
|
|
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
|
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
import java.io.*;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Paths;
|
|
import java.util.Arrays;
|
|
import java.util.LinkedList;
|
|
|
|
import static libKonogonka.Converter.getLEint;
|
|
import static libKonogonka.Converter.getLElong;
|
|
|
|
public class PFS0Provider implements IPFS0Provider{
|
|
private final static Logger log = LogManager.getLogger(PFS0Provider.class);
|
|
|
|
private String magic;
|
|
private int filesCount;
|
|
private int stringTableSize;
|
|
private byte[] padding;
|
|
private PFS0subFile[] pfs0subFiles;
|
|
//---------------------------------------
|
|
private long rawBlockDataStart;
|
|
|
|
private final File file;
|
|
private long offsetPositionInFile;
|
|
private long mediaStartOffset; // In 512-blocks
|
|
private long mediaEndOffset; // In 512-blocks
|
|
|
|
private long ncaOffset;
|
|
private BufferedInputStream stream;
|
|
private SuperBlockPFS0 superBlockPFS0;
|
|
private AesCtrDecryptSimple decryptor;
|
|
|
|
private LinkedList<byte[]> pfs0SHA256hashes;
|
|
|
|
private boolean encrypted;
|
|
|
|
public PFS0Provider(File nspFile) throws Exception{
|
|
this.file = nspFile;
|
|
createBufferedInputStream();
|
|
readPfs0Header();
|
|
}
|
|
|
|
public PFS0Provider(File file,
|
|
long ncaOffset,
|
|
SuperBlockPFS0 superBlockPFS0,
|
|
long mediaStartOffset,
|
|
long mediaEndOffset) throws Exception{
|
|
this.file = file;
|
|
this.ncaOffset = ncaOffset;
|
|
this.superBlockPFS0 = superBlockPFS0;
|
|
this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
|
|
this.mediaStartOffset = mediaStartOffset;
|
|
this.mediaEndOffset = mediaEndOffset;
|
|
this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
|
|
//bufferedInputStream = new BufferedInputStream(Files.newInputStream(fileWithPfs0.toPath()));
|
|
createBufferedInputStream();
|
|
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
|
if (toSkip != stream.skip(toSkip))
|
|
throw new Exception("Can't skip bytes prior Hash Table offset");
|
|
collectHashes();
|
|
|
|
createBufferedInputStream();
|
|
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
|
if (toSkip != stream.skip(toSkip))
|
|
throw new Exception("Can't skip bytes prior PFS0 offset");
|
|
readPfs0Header();
|
|
}
|
|
|
|
public PFS0Provider(File file,
|
|
long ncaOffset,
|
|
SuperBlockPFS0 superBlockPFS0,
|
|
AesCtrDecryptSimple decryptor,
|
|
long mediaStartOffset,
|
|
long mediaEndOffset
|
|
) throws Exception {
|
|
this.file = file;
|
|
this.ncaOffset = ncaOffset;
|
|
this.superBlockPFS0 = superBlockPFS0;
|
|
this.decryptor = decryptor;
|
|
this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
|
|
this.mediaStartOffset = mediaStartOffset;
|
|
this.mediaEndOffset = mediaEndOffset;
|
|
this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
|
|
this.encrypted = true;
|
|
|
|
createAesCtrEncryptedBufferedInputStream();
|
|
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
|
if (toSkip != stream.skip(toSkip))
|
|
throw new Exception("Can't skip bytes prior Hash Table offset");
|
|
collectHashes();
|
|
|
|
createAesCtrEncryptedBufferedInputStream();
|
|
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
|
if (toSkip != stream.skip(toSkip))
|
|
throw new Exception("Can't skip bytes prior PFS0 offset");
|
|
readPfs0Header();
|
|
}
|
|
|
|
private void readPfs0Header()throws Exception{
|
|
byte[] fileStartingBytes = new byte[0x10];
|
|
if (0x10 != stream.read(fileStartingBytes))
|
|
throw new Exception("Reading stream suddenly ended while trying to read starting 0x10 bytes");
|
|
|
|
// Update position
|
|
rawBlockDataStart += 0x10;
|
|
// Check PFS0Provider
|
|
magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
|
|
if (! magic.equals("PFS0")){
|
|
throw new Exception("Bad magic");
|
|
}
|
|
// Get files count
|
|
filesCount = getLEint(fileStartingBytes, 0x4);
|
|
if (filesCount <= 0 ) {
|
|
throw new Exception("Files count is too small");
|
|
}
|
|
// Get string table
|
|
stringTableSize = getLEint(fileStartingBytes, 0x8);
|
|
if (stringTableSize <= 0 ){
|
|
throw new Exception("String table is too small");
|
|
}
|
|
padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10);
|
|
//-------------------------------------------------------------------
|
|
pfs0subFiles = new PFS0subFile[filesCount];
|
|
|
|
long[] offsetsSubFiles = new long[filesCount];
|
|
long[] sizesSubFiles = new long[filesCount];
|
|
int[] strTableOffsets = new int[filesCount];
|
|
byte[][] zeroBytes = new byte[filesCount][];
|
|
|
|
byte[] fileEntryTable = new byte[0x18];
|
|
for (int i=0; i < filesCount; i++){
|
|
if (0x18 != stream.read(fileEntryTable))
|
|
throw new Exception("Reading stream suddenly ended while trying to read File Entry Table #"+i);
|
|
|
|
offsetsSubFiles[i] = getLElong(fileEntryTable, 0);
|
|
sizesSubFiles[i] = getLElong(fileEntryTable, 0x8);
|
|
strTableOffsets[i] = getLEint(fileEntryTable, 0x10);
|
|
zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18);
|
|
rawBlockDataStart += 0x18;
|
|
}
|
|
//*******************************************************************
|
|
// In here pointer in front of String table
|
|
String[] subFileNames = new String[filesCount];
|
|
byte[] stringTbl = new byte[stringTableSize];
|
|
if (stream.read(stringTbl) != stringTableSize){
|
|
throw new Exception("Read PFS0Provider String table failure. Can't read requested string table size ("+stringTableSize+")");
|
|
}
|
|
|
|
// Update position
|
|
rawBlockDataStart += stringTableSize;
|
|
|
|
for (int i=0; i < filesCount; i++){
|
|
int j = 0;
|
|
while (stringTbl[strTableOffsets[i]+j] != (byte)0x00)
|
|
j++;
|
|
subFileNames[i] = new String(stringTbl, strTableOffsets[i], j, StandardCharsets.UTF_8);
|
|
}
|
|
for (int i = 0; i < filesCount; i++){
|
|
pfs0subFiles[i] = new PFS0subFile(
|
|
subFileNames[i],
|
|
offsetsSubFiles[i],
|
|
sizesSubFiles[i],
|
|
zeroBytes[i]);
|
|
}
|
|
stream.close();
|
|
}
|
|
|
|
private void createAesCtrEncryptedBufferedInputStream() throws Exception{
|
|
decryptor.reset();
|
|
this.stream = new AesCtrBufferedInputStream(
|
|
decryptor,
|
|
ncaOffset,
|
|
mediaStartOffset,
|
|
mediaEndOffset,
|
|
Files.newInputStream(file.toPath()));
|
|
}
|
|
|
|
private void createBufferedInputStream() throws Exception{
|
|
this.stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
|
}
|
|
|
|
private void collectHashes() throws Exception{
|
|
pfs0SHA256hashes = new LinkedList<>();
|
|
long hashTableOffset = superBlockPFS0.getHashTableOffset();
|
|
long hashTableSize = superBlockPFS0.getHashTableSize();
|
|
|
|
if (hashTableOffset > 0){
|
|
if (hashTableOffset != stream.skip(hashTableOffset))
|
|
throw new Exception("Unable to skip bytes till Hash Table Offset: "+hashTableOffset);
|
|
}
|
|
for (int i = 0; i < hashTableSize / 0x20; i++){
|
|
byte[] sectionHash = new byte[0x20];
|
|
if (0x20 != stream.read(sectionHash))
|
|
throw new Exception("Unable to read hash");
|
|
pfs0SHA256hashes.add(sectionHash);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isEncrypted() { return true; }
|
|
@Override
|
|
public String getMagic() { return magic; }
|
|
@Override
|
|
public int getFilesCount() { return filesCount; }
|
|
@Override
|
|
public int getStringTableSize() { return stringTableSize; }
|
|
@Override
|
|
public byte[] getPadding() { return padding; }
|
|
@Override
|
|
public long getRawFileDataStart() { return rawBlockDataStart;}
|
|
@Override
|
|
public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; }
|
|
@Override
|
|
public File getFile(){ return file; }
|
|
|
|
@Override
|
|
public boolean exportContent(String saveToLocation, String subFileName){
|
|
for (int i = 0; i < pfs0subFiles.length; i++){
|
|
if (pfs0subFiles[i].getName().equals(subFileName))
|
|
return exportContent(saveToLocation, i);
|
|
}
|
|
return false;
|
|
}
|
|
@Override
|
|
public boolean exportContent(String saveToLocation, int subFileNumber){
|
|
PFS0subFile subFile = pfs0subFiles[subFileNumber];
|
|
File location = new File(saveToLocation);
|
|
location.mkdirs();
|
|
|
|
try (BufferedOutputStream extractedFileBOS = new BufferedOutputStream(
|
|
Files.newOutputStream(Paths.get(saveToLocation+File.separator+subFile.getName())))){
|
|
if (encrypted)
|
|
createAesCtrEncryptedBufferedInputStream();
|
|
else
|
|
createBufferedInputStream();
|
|
|
|
long subFileSize = subFile.getSize();
|
|
|
|
long toSkip = subFile.getOffset() + mediaStartOffset * 0x200 + rawBlockDataStart;
|
|
if (toSkip != stream.skip(toSkip))
|
|
throw new Exception("Unable to skip offset: "+toSkip);
|
|
|
|
int blockSize = 0x200;
|
|
if (subFileSize < 0x200)
|
|
blockSize = (int) subFileSize;
|
|
|
|
long i = 0;
|
|
byte[] block = new byte[blockSize];
|
|
|
|
int actuallyRead;
|
|
while (true) {
|
|
if ((actuallyRead = stream.read(block)) != blockSize)
|
|
throw new Exception("Read failure. Block Size: "+blockSize+", actuallyRead: "+actuallyRead);
|
|
extractedFileBOS.write(block);
|
|
i += blockSize;
|
|
if ((i + blockSize) > subFileSize) {
|
|
blockSize = (int) (subFileSize - i);
|
|
if (blockSize == 0)
|
|
break;
|
|
block = new byte[blockSize];
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e){
|
|
log.error("File export failure", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//TODO: REMOVE
|
|
@Override
|
|
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {return null;}
|
|
@Override
|
|
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception {return null;}
|
|
|
|
|
|
public LinkedList<byte[]> getPfs0SHA256hashes() {
|
|
return pfs0SHA256hashes;
|
|
}
|
|
|
|
public void printDebug(){
|
|
log.debug(".:: PFS0Provider ::.\n" +
|
|
"File name: " + file.getName() + "\n" +
|
|
"Raw block data start " + RainbowDump.formatDecHexString(rawBlockDataStart) + "\n" +
|
|
"Magic " + magic + "\n" +
|
|
"Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" +
|
|
"String Table Size " + RainbowDump.formatDecHexString(stringTableSize) + "\n" +
|
|
"Padding " + Converter.byteArrToHexString(padding) + "\n\n" +
|
|
|
|
"Offset position in file " + RainbowDump.formatDecHexString(offsetPositionInFile) + "\n" +
|
|
"Media Start Offset " + RainbowDump.formatDecHexString(mediaStartOffset) + "\n" +
|
|
"Media End Offset " + RainbowDump.formatDecHexString(mediaEndOffset) + "\n"
|
|
);
|
|
for (PFS0subFile subFile : pfs0subFiles){
|
|
log.debug(
|
|
"\nName: " + subFile.getName() + "\n" +
|
|
"Offset " + RainbowDump.formatDecHexString(subFile.getOffset()) + "\n" +
|
|
"Size " + RainbowDump.formatDecHexString(subFile.getSize()) + "\n" +
|
|
"Zeroes " + Converter.byteArrToHexString(subFile.getZeroes()) + "\n" +
|
|
"----------------------------------------------------------------"
|
|
);
|
|
}
|
|
}
|
|
} |