diff options
Diffstat (limited to 'src/com/sneed/pkrandom/ctr/NCCH.java')
-rw-r--r-- | src/com/sneed/pkrandom/ctr/NCCH.java | 1024 |
1 files changed, 0 insertions, 1024 deletions
diff --git a/src/com/sneed/pkrandom/ctr/NCCH.java b/src/com/sneed/pkrandom/ctr/NCCH.java deleted file mode 100644 index 8db5a4e..0000000 --- a/src/com/sneed/pkrandom/ctr/NCCH.java +++ /dev/null @@ -1,1024 +0,0 @@ -package com.sneed.pkrandom.ctr; - -/*----------------------------------------------------------------------------*/ -/*-- NCCH.java - a base class for dealing with 3DS NCCH ROM images. --*/ -/*-- --*/ -/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ -/*-- Pokemon and any associated names and the like are --*/ -/*-- trademark and (C) Nintendo 1996-2020. --*/ -/*-- --*/ -/*-- 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/>. --*/ -/*----------------------------------------------------------------------------*/ - -import com.sneed.pkrandom.FileFunctions; -import com.sneed.pkrandom.SysConstants; -import com.sneed.pkrandom.exceptions.CannotWriteToLocationException; -import com.sneed.pkrandom.exceptions.EncryptedROMException; -import com.sneed.pkrandom.exceptions.RandomizerIOException; -import cuecompressors.BLZCoder; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.*; -import java.util.*; - -public class NCCH { - private String romFilename; - private RandomAccessFile baseRom; - private long ncchStartingOffset; - private String productCode; - private String titleId; - private int version; - private long exefsOffset, romfsOffset, fileDataOffset; - private ExefsFileHeader codeFileHeader; - private SMDH smdh; - private List<ExefsFileHeader> extraExefsFiles; - private List<FileMetadata> fileMetadataList; - private Map<String, RomfsFile> romfsFiles; - private boolean romOpen; - private String tmpFolder; - private boolean writingEnabled; - private boolean codeCompressed, codeOpen, codeChanged; - private byte[] codeRamstored; - - // Public so the base game can read it from the game update NCCH - public long originalCodeCRC, originalRomfsHeaderCRC; - - private static final int media_unit_size = 0x200; - private static final int header_and_exheader_size = 0xA00; - private static final int ncsd_magic = 0x4E435344; - private static final int cia_header_size = 0x2020; - private static final int ncch_magic = 0x4E434348; - private static final int ncch_and_ncsd_magic_offset = 0x100; - private static final int exefs_header_size = 0x200; - private static final int romfs_header_size = 0x5C; - private static final int romfs_magic_1 = 0x49564643; - private static final int romfs_magic_2 = 0x00000100; - private static final int level3_header_size = 0x28; - private static final int metadata_unused = 0xFFFFFFFF; - - public NCCH(String filename, String productCode, String titleId) throws IOException { - this.romFilename = filename; - this.baseRom = new RandomAccessFile(filename, "r"); - this.ncchStartingOffset = NCCH.getCXIOffsetInFile(filename); - this.productCode = productCode; - this.titleId = titleId; - this.romOpen = true; - - if (this.ncchStartingOffset != -1) { - this.version = this.readVersionFromFile(); - } - - // TMP folder? - String rawFilename = new File(filename).getName(); - String dataFolder = "tmp_" + rawFilename.substring(0, rawFilename.lastIndexOf('.')); - // remove nonsensical chars - dataFolder = dataFolder.replaceAll("[^A-Za-z0-9_]+", ""); - File tmpFolder = new File(SysConstants.ROOT_PATH + dataFolder); - tmpFolder.mkdirs(); - if (tmpFolder.canWrite()) { - writingEnabled = true; - this.tmpFolder = SysConstants.ROOT_PATH + dataFolder + File.separator; - tmpFolder.deleteOnExit(); - } else { - writingEnabled = false; - } - - // The below code handles things "wrong" with regards to encrypted ROMs. We just - // blindly treat the ROM as decrypted and try to parse all of its data, when we - // *should* be looking at the header of the ROM to determine if the ROM is encrypted. - // Unfortunately, many people have poorly-decrypted ROMs that do not properly set - // the bytes on the NCCH header, so we can't assume that the header is telling the - // truth. If we read the whole ROM without crashing, then it's probably decrypted. - try { - readFileSystem(); - } catch (Exception ex) { - if (!this.isDecrypted()) { - throw new EncryptedROMException(ex); - } else { - throw ex; - } - } - } - - public void reopenROM() throws IOException { - if (!this.romOpen) { - baseRom = new RandomAccessFile(this.romFilename, "r"); - romOpen = true; - } - } - - public void closeROM() throws IOException { - if (this.romOpen && baseRom != null) { - baseRom.close(); - baseRom = null; - romOpen = false; - } - } - - private void readFileSystem() throws IOException { - exefsOffset = ncchStartingOffset + FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x1A0) * media_unit_size; - romfsOffset = ncchStartingOffset + FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x1B0) * media_unit_size; - baseRom.seek(ncchStartingOffset + 0x20D); - byte systemControlInfoFlags = baseRom.readByte(); - codeCompressed = (systemControlInfoFlags & 0x01) != 0; - readExefs(); - readRomfs(); - } - - private void readExefs() throws IOException { - System.out.println("NCCH: Reading exefs..."); - byte[] exefsHeaderData = new byte[exefs_header_size]; - baseRom.seek(exefsOffset); - baseRom.readFully(exefsHeaderData); - - ExefsFileHeader[] fileHeaders = new ExefsFileHeader[10]; - for (int i = 0; i < 10; i++) { - fileHeaders[i] = new ExefsFileHeader(exefsHeaderData, i * 0x10); - } - - extraExefsFiles = new ArrayList<>(); - for (ExefsFileHeader fileHeader : fileHeaders) { - if (fileHeader.isValid() && fileHeader.filename.equals(".code")) { - codeFileHeader = fileHeader; - } else if (fileHeader.isValid()) { - extraExefsFiles.add(fileHeader); - } - - if (fileHeader.isValid() && fileHeader.filename.equals("icon")) { - byte[] smdhBytes = new byte[fileHeader.size]; - baseRom.seek(exefsOffset + 0x200 + fileHeader.offset); - baseRom.readFully(smdhBytes); - smdh = new SMDH(smdhBytes); - } - } - System.out.println("NCCH: Done reading exefs"); - } - - private void readRomfs() throws IOException { - System.out.println("NCCH: Reading romfs..."); - byte[] romfsHeaderData = new byte[romfs_header_size]; - baseRom.seek(romfsOffset); - baseRom.readFully(romfsHeaderData); - originalRomfsHeaderCRC = FileFunctions.getCRC32(romfsHeaderData); - int magic1 = FileFunctions.readFullIntBigEndian(romfsHeaderData, 0x00); - int magic2 = FileFunctions.readFullIntBigEndian(romfsHeaderData, 0x04); - if (magic1 != romfs_magic_1 || magic2 != romfs_magic_2) { - System.err.println("NCCH: romfs does not contain magic values"); - // Not a valid romfs - return; - } - int masterHashSize = FileFunctions.readFullInt(romfsHeaderData, 0x08); - int level3HashBlockSize = 1 << FileFunctions.readFullInt(romfsHeaderData, 0x4C); - long level3Offset = romfsOffset + alignLong(0x60 + masterHashSize, level3HashBlockSize); - - byte[] level3HeaderData = new byte[level3_header_size]; - baseRom.seek(level3Offset); - baseRom.readFully(level3HeaderData); - int headerLength = FileFunctions.readFullInt(level3HeaderData, 0x00); - if (headerLength != level3_header_size) { - // Not a valid romfs - System.err.println("NCCH: romfs does not have a proper level 3 header"); - return; - } - int directoryMetadataOffset = FileFunctions.readFullInt(level3HeaderData, 0x0C); - int directoryMetadataLength = FileFunctions.readFullInt(level3HeaderData, 0x10); - int fileMetadataOffset = FileFunctions.readFullInt(level3HeaderData, 0x1c); - int fileMetadataLength = FileFunctions.readFullInt(level3HeaderData, 0x20); - int fileDataOffsetFromHeaderStart = FileFunctions.readFullInt(level3HeaderData, 0x24); - fileDataOffset = level3Offset + fileDataOffsetFromHeaderStart; - - byte[] directoryMetadataBlock = new byte[directoryMetadataLength]; - baseRom.seek(level3Offset + directoryMetadataOffset); - baseRom.readFully(directoryMetadataBlock); - byte[] fileMetadataBlock = new byte[fileMetadataLength]; - baseRom.seek(level3Offset + fileMetadataOffset); - baseRom.readFully(fileMetadataBlock); - fileMetadataList = new ArrayList<>(); - romfsFiles = new TreeMap<>(); - visitDirectory(0, "", directoryMetadataBlock, fileMetadataBlock); - System.out.println("NCCH: Done reading romfs"); - } - - private void visitDirectory(int offset, String rootPath, byte[] directoryMetadataBlock, byte[] fileMetadataBlock) { - DirectoryMetadata metadata = new DirectoryMetadata(directoryMetadataBlock, offset); - String currentPath = rootPath; - if (!metadata.name.equals("")) { - currentPath = rootPath + metadata.name + "/"; - } - - if (metadata.firstFileOffset != metadata_unused) { - visitFile(metadata.firstFileOffset, currentPath, fileMetadataBlock); - } - if (metadata.firstChildDirectoryOffset != metadata_unused) { - visitDirectory(metadata.firstChildDirectoryOffset, currentPath, directoryMetadataBlock, fileMetadataBlock); - } - if (metadata.siblingDirectoryOffset != metadata_unused) { - visitDirectory(metadata.siblingDirectoryOffset, rootPath, directoryMetadataBlock, fileMetadataBlock); - } - } - - private void visitFile(int offset, String rootPath, byte[] fileMetadataBlock) { - FileMetadata metadata = new FileMetadata(fileMetadataBlock, offset); - String currentPath = rootPath + metadata.name; - System.out.println("NCCH: Visiting file " + currentPath); - RomfsFile file = new RomfsFile(this); - file.offset = fileDataOffset + metadata.fileDataOffset; - file.size = (int) metadata.fileDataLength; // no Pokemon game has a file larger than unsigned int max - file.fullPath = currentPath; - metadata.file = file; - fileMetadataList.add(metadata); - romfsFiles.put(currentPath, file); - if (metadata.siblingFileOffset != metadata_unused) { - visitFile(metadata.siblingFileOffset, rootPath, fileMetadataBlock); - } - } - - public void saveAsNCCH(String filename, String gameAcronym, long seed) throws IOException, NoSuchAlgorithmException { - this.reopenROM(); - - // Initialize new ROM - RandomAccessFile fNew = new RandomAccessFile(filename, "rw"); - - // Read the header and exheader and write it to the output ROM - byte[] header = new byte[header_and_exheader_size]; - baseRom.seek(ncchStartingOffset); - baseRom.readFully(header); - fNew.write(header); - - // Just in case they were set wrong in the original header, let's correctly set the - // bytes in the header to indicate the output ROM is decrypted - byte[] flags = new byte[8]; - baseRom.seek(ncchStartingOffset + 0x188); - baseRom.readFully(flags); - flags[3] = 0; - flags[7] = 4; - fNew.seek(0x188); - fNew.write(flags); - - // The logo is small enough (8KB) to just read the whole thing into memory. Write it to the new ROM directly - // after the header, then update the new ROM's logo offset - long logoOffset = ncchStartingOffset + FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x198) * media_unit_size; - long logoLength = FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x19C) * media_unit_size; - if (logoLength > 0) { - byte[] logo = new byte[(int) logoLength]; - baseRom.seek(logoOffset); - baseRom.readFully(logo); - long newLogoOffset = header_and_exheader_size; - fNew.seek(newLogoOffset); - fNew.write(logo); - fNew.seek(0x198); - fNew.write((int) newLogoOffset / media_unit_size); - } - - // The plain region is even smaller (1KB) so repeat the same process - long plainOffset = ncchStartingOffset + FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x190) * media_unit_size; - long plainLength = FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x194) * media_unit_size; - if (plainLength > 0) { - byte[] plain = new byte[(int) plainLength]; - baseRom.seek(plainOffset); - baseRom.readFully(plain); - long newPlainOffset = header_and_exheader_size + logoLength; - fNew.seek(newPlainOffset); - fNew.write(plain); - fNew.seek(0x190); - fNew.write((int) newPlainOffset / media_unit_size); - } - - // Update the SMDH so that Citra displays the seed in the title - smdh.setAllDescriptions(gameAcronym + " randomizer seed: " + seed); - smdh.setAllPublishers("Universal Pokemon Randomizer ZX"); - - // Now, reconstruct the exefs based on our new version of .code and our new SMDH - long newExefsOffset = header_and_exheader_size + logoLength + plainLength; - long newExefsLength = rebuildExefs(fNew, newExefsOffset); - fNew.seek(0x1A0); - fNew.write((int) newExefsOffset / media_unit_size); - fNew.seek(0x1A4); - fNew.write((int) newExefsLength / media_unit_size); - - // Then, reconstruct the romfs - // TODO: Fix the yet-unsolved alignment issues in rebuildRomfs when you remove this align - long newRomfsOffset = alignLong(header_and_exheader_size + logoLength + plainLength + newExefsLength, 4096); - long newRomfsLength = rebuildRomfs(fNew, newRomfsOffset); - fNew.seek(0x1B0); - fNew.write((int) newRomfsOffset / media_unit_size); - fNew.seek(0x1B4); - fNew.write((int) newRomfsLength / media_unit_size); - - // Lastly, reconstruct the superblock hashes - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - int exefsHashRegionSize = FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x1A8) * media_unit_size; - byte[] exefsDataToHash = new byte[exefsHashRegionSize]; - fNew.seek(newExefsOffset); - fNew.readFully(exefsDataToHash); - byte[] exefsSuperblockHash = digest.digest(exefsDataToHash); - fNew.seek(0x1C0); - fNew.write(exefsSuperblockHash); - int romfsHashRegionSize = FileFunctions.readIntFromFile(baseRom, ncchStartingOffset + 0x1B8) * media_unit_size; - byte[] romfsDataToHash = new byte[romfsHashRegionSize]; - fNew.seek(newRomfsOffset); - fNew.readFully(romfsDataToHash); - byte[] romfsSuperblockHash = digest.digest(romfsDataToHash); - fNew.seek(0x1E0); - fNew.write(romfsSuperblockHash); - - // While totally optional, let's zero out the NCCH signature so that - // it's clear this isn't a properly-signed ROM - byte[] zeroedSignature = new byte[0x100]; - fNew.seek(0x0); - fNew.write(zeroedSignature); - fNew.close(); - } - - private long rebuildExefs(RandomAccessFile fNew, long newExefsOffset) throws IOException, NoSuchAlgorithmException { - System.out.println("NCCH: Rebuilding exefs..."); - byte[] code = getCode(); - if (codeCompressed) { - code = new BLZCoder(null).BLZ_EncodePub(code, false, true, ".code"); - } - - // Create a new ExefsFileHeader for our updated .code - ExefsFileHeader newCodeHeader = new ExefsFileHeader(); - newCodeHeader.filename = codeFileHeader.filename; - newCodeHeader.size = code.length; - newCodeHeader.offset = 0; - - // For all the file headers, write them to the new ROM and store them in order for hashing later - ExefsFileHeader[] newHeaders = new ExefsFileHeader[10]; - newHeaders[0] = newCodeHeader; - fNew.seek(newExefsOffset); - fNew.write(newCodeHeader.asBytes()); - for (int i = 0; i < extraExefsFiles.size(); i++) { - ExefsFileHeader header = extraExefsFiles.get(i); - newHeaders[i + 1] = header; - fNew.write(header.asBytes()); - } - - // Write the file data, then hash the data and write the hashes in reverse order - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - long endingOffset = 0; - for (int i = 0; i < newHeaders.length; i++) { - ExefsFileHeader header = newHeaders[i]; - if (header != null) { - byte[] data; - if (header.filename.equals(".code")) { - data = code; - } else if (header.filename.equals("icon")) { - data = smdh.getBytes(); - } else { - long dataOffset = exefsOffset + 0x200 + header.offset; - data = new byte[header.size]; - baseRom.seek(dataOffset); - baseRom.readFully(data); - } - fNew.seek(newExefsOffset + 0x200 + header.offset); - fNew.write(data); - byte[] hash = digest.digest(data); - fNew.seek(newExefsOffset + 0x200 - ((i + 1) * 0x20)); - fNew.write(hash); - endingOffset = newExefsOffset + 0x200 + header.offset + header.size; - } - } - - // Pad to media unit size - fNew.seek(endingOffset); - long exefsLength = endingOffset - newExefsOffset; - while (exefsLength % media_unit_size != 0) { - fNew.writeByte(0); - exefsLength++; - } - - System.out.println("NCCH: Done rebuilding exefs"); - return exefsLength; - } - - private long rebuildRomfs(RandomAccessFile fNew, long newRomfsOffset) throws IOException, NoSuchAlgorithmException { - System.out.println("NCCH: Rebuilding romfs..."); - - // Start by copying the romfs header straight from the original ROM. We'll update the - // header as we continue to build the romfs - byte[] romfsHeaderData = new byte[romfs_header_size]; - baseRom.seek(romfsOffset); - baseRom.readFully(romfsHeaderData); - fNew.seek(newRomfsOffset); - fNew.write(romfsHeaderData); - - // Now find the level 3 (file data) offset, since the first thing we need to do is write the - // updated file data. We're assuming here that the master hash size is smaller than the level 3 - // hash block size, which it almost certainly will because we're not adding large amounts of data - // to the romfs - int masterHashSize = FileFunctions.readFullInt(romfsHeaderData, 0x08); - int level3HashBlockSize = 1 << FileFunctions.readFullInt(romfsHeaderData, 0x4C); - long level3Offset = romfsOffset + alignLong(0x60 + masterHashSize, level3HashBlockSize); - long newLevel3Offset = newRomfsOffset + alignLong(0x60 + masterHashSize, level3HashBlockSize); - - // Copy the level 3 header straight from the original ROM. Since we're not adding or - // removing any files, the File/Directory tables should have the same offsets and lengths - byte[] level3HeaderData = new byte[level3_header_size]; - baseRom.seek(level3Offset); - baseRom.readFully(level3HeaderData); - fNew.seek(newLevel3Offset); - fNew.write(level3HeaderData); - - // Write out both hash tables and the directory metadata table. Since we're not adding or removing - // any files/directories, we can just use what's in the base ROM for this. - int directoryHashTableOffset = FileFunctions.readFullInt(level3HeaderData, 0x04); - int directoryHashTableLength = FileFunctions.readFullInt(level3HeaderData, 0x08); - int directoryMetadataTableOffset = FileFunctions.readFullInt(level3HeaderData, 0x0C); - int directoryMetadataTableLength = FileFunctions.readFullInt(level3HeaderData, 0x10); - int fileHashTableOffset = FileFunctions.readFullInt(level3HeaderData, 0x14); - int fileHashTableLength = FileFunctions.readFullInt(level3HeaderData, 0x18); - byte[] directoryHashTable = new byte[directoryHashTableLength]; - baseRom.seek(level3Offset + directoryHashTableOffset); - baseRom.readFully(directoryHashTable); - fNew.seek(newLevel3Offset + directoryHashTableOffset); - fNew.write(directoryHashTable); - byte[] directoryMetadataTable = new byte[directoryMetadataTableLength]; - baseRom.seek(level3Offset + directoryMetadataTableOffset); - baseRom.readFully(directoryMetadataTable); - fNew.seek(newLevel3Offset + directoryMetadataTableOffset); - fNew.write(directoryMetadataTable); - byte[] fileHashTable = new byte[fileHashTableLength]; - baseRom.seek(level3Offset + fileHashTableOffset); - baseRom.readFully(fileHashTable); - fNew.seek(newLevel3Offset + fileHashTableOffset); - fNew.write(fileHashTable); - - // Now reconstruct the file metadata table. It may need to be changed if any file grew or shrunk - int fileMetadataTableOffset = FileFunctions.readFullInt(level3HeaderData, 0x1C); - int fileMetadataTableLength = FileFunctions.readFullInt(level3HeaderData, 0x20); - byte[] newFileMetadataTable = updateFileMetadataTable(fileMetadataTableLength); - fNew.seek(newLevel3Offset + fileMetadataTableOffset); - fNew.write(newFileMetadataTable); - - // Using the new file metadata table, output the file data - int fileDataOffset = FileFunctions.readFullInt(level3HeaderData, 0x24); - long endOfFileDataOffset = 0; - for (FileMetadata metadata : fileMetadataList) { - System.out.println("NCCH: Writing file " + metadata.file.fullPath + " to romfs"); - // Users have sent us bug reports with really bizarre errors here that seem to indicate - // broken metadata; do this in a try-catch solely so we can log the metadata if we fail - try { - byte[] fileData; - if (metadata.file.fileChanged) { - fileData = metadata.file.getOverrideContents(); - } else { - fileData = new byte[metadata.file.size]; - baseRom.seek(metadata.file.offset); - baseRom.readFully(fileData); - } - long currentDataOffset = newLevel3Offset + fileDataOffset + metadata.fileDataOffset; - fNew.seek(currentDataOffset); - fNew.write(fileData); - endOfFileDataOffset = currentDataOffset + fileData.length; - } catch (Exception e) { - String message = String.format("Error when building romfs: File: %s, offset: %s, size: %s", - metadata.file.fullPath, metadata.offset, metadata.file.size); - throw new RandomizerIOException(message, e); - } - } - - // Now that level 3 (file data) is done, construct level 2 (hashes of file data) - // Note that in the ROM, level 1 comes *before* level 2, so we need to calculate - // level 1 length and offset as well. - long newLevel3EndingOffset = endOfFileDataOffset; - long newLevel3HashdataSize = newLevel3EndingOffset - newLevel3Offset; - long numberOfLevel3HashBlocks = alignLong(newLevel3HashdataSize, level3HashBlockSize) / level3HashBlockSize; - int level2HashBlockSize = 1 << FileFunctions.readFullInt(romfsHeaderData, 0x34); - long newLevel2HashdataSize = numberOfLevel3HashBlocks * 0x20; - long numberOfLevel2HashBlocks = alignLong(newLevel2HashdataSize, level2HashBlockSize) / level2HashBlockSize; - int level1HashBlockSize = 1 << FileFunctions.readFullInt(romfsHeaderData, 0x1C); - long newLevel1HashdataSize = numberOfLevel2HashBlocks * 0x20; - long newLevel1Offset = newLevel3Offset + alignLong(newLevel3HashdataSize, level3HashBlockSize); - long newLevel2Offset = newLevel1Offset + alignLong(newLevel1HashdataSize, level1HashBlockSize); - long newFileEndingOffset = alignLong(newLevel2Offset + newLevel2HashdataSize, level2HashBlockSize); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] dataToHash = new byte[level3HashBlockSize]; - for (long i = 0; i < numberOfLevel3HashBlocks; i++) { - fNew.seek(newLevel3Offset + (i * level3HashBlockSize)); - fNew.readFully(dataToHash); - byte[] hash = digest.digest(dataToHash); - fNew.seek(newLevel2Offset + (i * 0x20)); - fNew.write(hash); - } - while (fNew.getFilePointer() != newFileEndingOffset) { - fNew.writeByte(0); - } - - // Now that level 2 (hashes of file data) is done, construct level 1 (hashes of - // hashes of file data) and the master hash/level 0 (hashes of level 1) - dataToHash = new byte[level2HashBlockSize]; - for (long i = 0; i < numberOfLevel2HashBlocks; i++) { - fNew.seek(newLevel2Offset + (i * level2HashBlockSize)); - fNew.readFully(dataToHash); - byte[] hash = digest.digest(dataToHash); - fNew.seek(newLevel1Offset + (i * 0x20)); - fNew.write(hash); - } - long numberOfLevel1HashBlocks = alignLong(newLevel1HashdataSize, level1HashBlockSize) / level1HashBlockSize; - dataToHash = new byte[level1HashBlockSize]; - for (long i = 0; i < numberOfLevel1HashBlocks; i++) { - fNew.seek(newLevel1Offset + (i * level1HashBlockSize)); - fNew.readFully(dataToHash); - byte[] hash = digest.digest(dataToHash); - fNew.seek(newRomfsOffset + 0x60 + (i * 0x20)); - fNew.write(hash); - } - - // Lastly, update the header and return the size of the new romfs - long level1LogicalOffset = 0; - long level2LogicalOffset = alignLong(newLevel1HashdataSize, level1HashBlockSize); - long level3LogicalOffset = alignLong(level2LogicalOffset + newLevel2HashdataSize, level2HashBlockSize); - FileFunctions.writeFullInt(romfsHeaderData, 0x08, (int) numberOfLevel1HashBlocks * 0x20); - FileFunctions.writeFullLong(romfsHeaderData, 0x0C, level1LogicalOffset); - FileFunctions.writeFullLong(romfsHeaderData, 0x14, newLevel1HashdataSize); - FileFunctions.writeFullLong(romfsHeaderData, 0x24, level2LogicalOffset); - FileFunctions.writeFullLong(romfsHeaderData, 0x2C, newLevel2HashdataSize); - FileFunctions.writeFullLong(romfsHeaderData, 0x3C, level3LogicalOffset); - FileFunctions.writeFullLong(romfsHeaderData, 0x44, newLevel3HashdataSize); - fNew.seek(newRomfsOffset); - fNew.write(romfsHeaderData); - long currentLength = newFileEndingOffset - newRomfsOffset; - long newRomfsLength = alignLong(currentLength, media_unit_size); - fNew.seek(newFileEndingOffset); - while (fNew.getFilePointer() < newRomfsOffset + newRomfsLength) { - fNew.writeByte(0); - } - - System.out.println("NCCH: Done rebuilding romfs"); - return newRomfsLength; - } - - private byte[] updateFileMetadataTable(int fileMetadataTableLength) { - fileMetadataList.sort((FileMetadata f1, FileMetadata f2) -> (int) (f1.fileDataOffset - f2.fileDataOffset)); - byte[] fileMetadataTable = new byte[fileMetadataTableLength]; - int currentTableOffset = 0; - long currentFileDataOffset = 0; - for (FileMetadata metadata : fileMetadataList) { - metadata.fileDataOffset = currentFileDataOffset; - if (metadata.file.fileChanged) { - metadata.fileDataLength = metadata.file.size; - } - byte[] metadataBytes = metadata.asBytes(); - System.arraycopy(metadataBytes, 0, fileMetadataTable, currentTableOffset, metadataBytes.length); - currentTableOffset += metadataBytes.length; - currentFileDataOffset += metadata.fileDataLength; - } - return fileMetadataTable; - } - - public void saveAsLayeredFS(String outputPath) throws IOException { - String layeredFSRootPath = outputPath + File.separator + titleId + File.separator; - File layeredFSRootDir = new File(layeredFSRootPath); - if (!layeredFSRootDir.exists()) { - layeredFSRootDir.mkdirs(); - } else { - purgeDirectory(layeredFSRootDir); - } - String romfsRootPath = layeredFSRootPath + "romfs" + File.separator; - File romfsDir = new File(romfsRootPath); - if (!romfsDir.exists()) { - romfsDir.mkdirs(); - } - - if (codeChanged) { - byte[] code = getCode(); - FileOutputStream fos = new FileOutputStream(new File(layeredFSRootPath + "code.bin")); - fos.write(code); - fos.close(); - } - - for (Map.Entry<String, RomfsFile> entry : romfsFiles.entrySet()) { - RomfsFile file = entry.getValue(); - if (file.fileChanged) { - writeRomfsFileToLayeredFS(file, romfsRootPath); - } - } - } - - private void purgeDirectory(File directory) { - for (File file : directory.listFiles()) { - if (file.isDirectory()) { - purgeDirectory(file); - } - file.delete(); - } - } - - private void writeRomfsFileToLayeredFS(RomfsFile file, String layeredFSRootPath) throws IOException { - String[] romfsPathComponents = file.fullPath.split("/"); - StringBuffer buffer = new StringBuffer(layeredFSRootPath); - for (int i = 0; i < romfsPathComponents.length - 1; i++) { - buffer.append(romfsPathComponents[i]); - buffer.append(File.separator); - File currentDir = new File(buffer.toString()); - if (!currentDir.exists()) { - currentDir.mkdirs(); - } - } - buffer.append(romfsPathComponents[romfsPathComponents.length - 1]); - String romfsFilePath = buffer.toString(); - FileOutputStream fos = new FileOutputStream(new File(romfsFilePath)); - fos.write(file.getOverrideContents()); - fos.close(); - } - - public boolean isDecrypted() throws IOException { - // This is the way you're *supposed* to tell if a ROM is decrypted. Specifically, this - // is checking the noCrypto flag on the NCCH bitflags. - long ncchFlagOffset = ncchStartingOffset + 0x188; - byte[] ncchFlags = new byte[8]; - baseRom.seek(ncchFlagOffset); - baseRom.readFully(ncchFlags); - if ((ncchFlags[7] & 0x4) != 0) { - return true; - } - - // However, some poorly-decrypted ROMs don't set this flag. So our heuristic for detecting - // if they're decrypted is to check whether the battle CRO exists, since all 3DS Pokemon - // games and updates have this file. If the game is *really* encrypted, then the odds of us - // successfully extracting this exact name from the metadata tables is like one in a billion. - return romfsFiles != null && (romfsFiles.containsKey("DllBattle.cro") || romfsFiles.containsKey("Battle.cro")); - } - - // Retrieves a decompressed version of .code (the game's executable). - // The first time this is called, it will retrieve it straight from the - // exefs. Future calls will rely on a cached version to speed things up. - // If writing is enabled, it will cache the decompressed version to the - // tmpFolder; otherwise, it will store it in RAM. - public byte[] getCode() throws IOException { - if (!codeOpen) { - codeOpen = true; - byte[] code = new byte[codeFileHeader.size]; - - // File header offsets are from the start of the exefs but *exclude* the - // size of the exefs header, so we need to add it back ourselves. - baseRom.seek(exefsOffset + exefs_header_size + codeFileHeader.offset); - baseRom.readFully(code); - originalCodeCRC = FileFunctions.getCRC32(code); - - if (codeCompressed) { - code = new BLZCoder(null).BLZ_DecodePub(code, ".code"); - } - - // Now actually make the copy or w/e - if (writingEnabled) { - File arm9file = new File(tmpFolder + ".code"); - FileOutputStream fos = new FileOutputStream(arm9file); - fos.write(code); - fos.close(); - arm9file.deleteOnExit(); - this.codeRamstored = null; - return code; - } else { - this.codeRamstored = code; - byte[] newcopy = new byte[code.length]; - System.arraycopy(code, 0, newcopy, 0, code.length); - return newcopy; - } - } else { - if (writingEnabled) { - return FileFunctions.readFileFullyIntoBuffer(tmpFolder + ".code"); - } else { - byte[] newcopy = new byte[this.codeRamstored.length]; - System.arraycopy(this.codeRamstored, 0, newcopy, 0, this.codeRamstored.length); - return newcopy; - } - } - } - - public void writeCode(byte[] code) throws IOException { - if (!codeOpen) { - getCode(); - } - codeChanged = true; - if (writingEnabled) { - FileOutputStream fos = new FileOutputStream(new File(tmpFolder + ".code")); - fos.write(code); - fos.close(); - } else { - if (this.codeRamstored.length == code.length) { - // copy new in - System.arraycopy(code, 0, this.codeRamstored, 0, code.length); - } else { - // make new array - this.codeRamstored = null; - this.codeRamstored = new byte[code.length]; - System.arraycopy(code, 0, this.codeRamstored, 0, code.length); - } - } - } - - public boolean hasFile(String filename) { - return romfsFiles.containsKey(filename); - } - - // returns null if file doesn't exist - public byte[] getFile(String filename) throws IOException { - if (romfsFiles.containsKey(filename)) { - return romfsFiles.get(filename).getContents(); - } else { - return null; - } - } - - public void writeFile(String filename, byte[] data) throws IOException { - if (romfsFiles.containsKey(filename)) { - romfsFiles.get(filename).writeOverride(data); - } - } - - public void printRomDiagnostics(PrintStream logStream, NCCH gameUpdate) { - Path p = Paths.get(this.romFilename); - logStream.println("File name: " + p.getFileName().toString()); - if (gameUpdate == null) { - logStream.println(".code: " + String.format("%08X", this.originalCodeCRC)); - } else { - logStream.println(".code: " + String.format("%08X", gameUpdate.originalCodeCRC)); - } - logStream.println("romfs header: " + String.format("%08X", this.originalRomfsHeaderCRC)); - if (gameUpdate != null) { - logStream.println("romfs header (game update): " + String.format("%08X", gameUpdate.originalRomfsHeaderCRC)); - } - List<String> fileList = new ArrayList<>(); - Map<String, String> baseRomfsFileDiagnostics = this.getRomfsFilesDiagnostics(); - Map<String, String> updateRomfsFileDiagnostics = new HashMap<>(); - if (gameUpdate != null) { - updateRomfsFileDiagnostics = gameUpdate.getRomfsFilesDiagnostics(); - } - for (Map.Entry<String, String> entry : updateRomfsFileDiagnostics.entrySet()) { - baseRomfsFileDiagnostics.remove(entry.getKey()); - fileList.add(entry.getValue()); - } - for (Map.Entry<String, String> entry : baseRomfsFileDiagnostics.entrySet()) { - fileList.add(entry.getValue()); - } - Collections.sort(fileList); - for (String fileLog : fileList) { - logStream.println(fileLog); - } - } - - public Map<String, String> getRomfsFilesDiagnostics() { - Map<String, String> fileDiagnostics = new HashMap<>(); - for (Map.Entry<String, RomfsFile> entry : romfsFiles.entrySet()) { - if (entry.getValue().originalCRC != 0) { - fileDiagnostics.put(entry.getKey(), entry.getKey() + ": " + String.format("%08X", entry.getValue().originalCRC)); - } - } - return fileDiagnostics; - } - - public String getTmpFolder() { - return tmpFolder; - } - - public RandomAccessFile getBaseRom() { - return baseRom; - } - - public boolean isWritingEnabled() { - return writingEnabled; - } - - public String getProductCode() { - return productCode; - } - - public String getTitleId() { - return titleId; - } - - public int getVersion() { - return version; - } - - public static int alignInt(int num, int alignment) { - int mask = ~(alignment - 1); - return (num + (alignment - 1)) & mask; - } - - public static long alignLong(long num, long alignment) { - long mask = ~(alignment - 1); - return (num + (alignment - 1)) & mask; - } - - private int readVersionFromFile() { - try { - // Only CIAs can define a version in their TMD. If this is a different ROM type, - // just exit out early. - int magic = FileFunctions.readBigEndianIntFromFile(this.baseRom, ncch_and_ncsd_magic_offset); - if (magic == ncch_magic || magic == ncsd_magic) { - return 0; - } - - // For CIAs, we need to read the title metadata (TMD) in order to retrieve the version. - // The TMD is after the certificate chain and ticket. - int certChainSize = FileFunctions.readIntFromFile(this.baseRom, 0x08); - int ticketSize = FileFunctions.readIntFromFile(this.baseRom, 0x0C); - long certChainOffset = NCCH.alignLong(cia_header_size, 64); - long ticketOffset = NCCH.alignLong(certChainOffset + certChainSize, 64); - long tmdOffset = NCCH.alignLong(ticketOffset + ticketSize, 64); - - // At the start of the TMD is a signature whose length varies based on what type of signature it is. - int signatureType = FileFunctions.readBigEndianIntFromFile(this.baseRom, tmdOffset); - int signatureSize, paddingSize; - switch (signatureType) { - case 0x010003: - signatureSize = 0x200; - paddingSize = 0x3C; - break; - case 0x010004: - signatureSize = 0x100; - paddingSize = 0x3C; - break; - case 0x010005: - signatureSize = 0x3C; - paddingSize = 0x40; - break; - default: - signatureSize = -1; - paddingSize = -1; - break; - } - if (signatureSize == -1) { - // This shouldn't happen in practice, since all used and valid signature types are represented - // in the above switch. However, if we can't find the right signature type, then it's probably - // an invalid CIA anyway, so we're unlikely to get good version information out of it. - return 0; - } - - // After the signature is the TMD header, which actually contains the version information. - long tmdHeaderOffset = tmdOffset + 4 + signatureSize + paddingSize; - return FileFunctions.read2ByteBigEndianIntFromFile(this.baseRom, tmdHeaderOffset + 0x9C); - } catch (IOException e) { - throw new RandomizerIOException(e); - } - } - - // At the bare minimum, a 3DS game consists of what's known as a CXI file, which - // is just an NCCH that contains executable code. However, 3DS games are packaged - // in various containers that can hold other NCCH files like the game manual and - // firmware updates, among other things. This function's determines the location - // of the CXI regardless of the container. - public static long getCXIOffsetInFile(String filename) { - try { - RandomAccessFile rom = new RandomAccessFile(filename, "r"); - int ciaHeaderSize = FileFunctions.readIntFromFile(rom, 0x00); - if (ciaHeaderSize == cia_header_size) { - // This *might* be a CIA; let's do our best effort to try to get - // a CXI out of this. - int certChainSize = FileFunctions.readIntFromFile(rom, 0x08); - int ticketSize = FileFunctions.readIntFromFile(rom, 0x0C); - int tmdFileSize = FileFunctions.readIntFromFile(rom, 0x10); - - // If this is *really* a CIA, we'll find our CXI at the beginning of the - // content section, which is after the certificate chain, ticket, and TMD - long certChainOffset = NCCH.alignLong(ciaHeaderSize, 64); - long ticketOffset = NCCH.alignLong(certChainOffset + certChainSize, 64); - long tmdOffset = NCCH.alignLong(ticketOffset + ticketSize, 64); - long contentOffset = NCCH.alignLong(tmdOffset + tmdFileSize, 64); - int magic = FileFunctions.readBigEndianIntFromFile(rom, contentOffset + ncch_and_ncsd_magic_offset); - if (magic == ncch_magic) { - // This CIA's content contains a valid CXI! - return contentOffset; - } - } - - // We don't put the following code in an else-block because there *might* - // exist a totally-valid CXI or CCI whose first four bytes just so - // *happen* to be the same as the first four bytes of a CIA file. - int magic = FileFunctions.readBigEndianIntFromFile(rom, ncch_and_ncsd_magic_offset); - rom.close(); - if (magic == ncch_magic) { - // Magic is NCCH, so this just a straight-up NCCH/CXI; there is no container - // around the game data. Thus, the CXI offset is the beginning of the file. - return 0; - } else if (magic == ncsd_magic) { - // Magic is NCSD, so this is almost certainly a CCI. The CXI is always - // a fixed distance away from the start. - return 0x4000; - } else { - // This doesn't seem to be a valid 3DS file. - return -1; - } - } catch (IOException e) { - throw new RandomizerIOException(e); - } - } - - private class ExefsFileHeader { - public String filename; - public int offset; - public int size; - - public ExefsFileHeader() { } - - public ExefsFileHeader(byte[] exefsHeaderData, int fileHeaderOffset) { - byte[] filenameBytes = new byte[0x8]; - System.arraycopy(exefsHeaderData, fileHeaderOffset, filenameBytes, 0, 0x8); - this.filename = new String(filenameBytes, StandardCharsets.UTF_8).trim(); - this.offset = FileFunctions.readFullInt(exefsHeaderData, fileHeaderOffset + 0x08); - this.size = FileFunctions.readFullInt(exefsHeaderData, fileHeaderOffset + 0x0C); - } - - public boolean isValid() { - return this.filename != "" && this.size != 0; - } - - public byte[] asBytes() { - byte[] output = new byte[0x10]; - byte[] filenameBytes = this.filename.getBytes(StandardCharsets.UTF_8); - System.arraycopy(filenameBytes, 0, output, 0, filenameBytes.length); - FileFunctions.writeFullInt(output, 0x08, this.offset); - FileFunctions.writeFullInt(output, 0x0C, this.size); - return output; - } - } - - private class DirectoryMetadata { - public int parentDirectoryOffset; - public int siblingDirectoryOffset; - public int firstChildDirectoryOffset; - public int firstFileOffset; - public int nextDirectoryInHashBucketOffset; - public int nameLength; - public String name; - - public DirectoryMetadata(byte[] directoryMetadataBlock, int offset) { - parentDirectoryOffset = FileFunctions.readFullInt(directoryMetadataBlock, offset); - siblingDirectoryOffset = FileFunctions.readFullInt(directoryMetadataBlock, offset + 0x04); - firstChildDirectoryOffset = FileFunctions.readFullInt(directoryMetadataBlock, offset + 0x08); - firstFileOffset = FileFunctions.readFullInt(directoryMetadataBlock, offset + 0x0C); - nextDirectoryInHashBucketOffset = FileFunctions.readFullInt(directoryMetadataBlock, offset + 0x10); - nameLength = FileFunctions.readFullInt(directoryMetadataBlock, offset + 0x14); - name = ""; - if (nameLength != metadata_unused) { - byte[] nameBytes = new byte[nameLength]; - System.arraycopy(directoryMetadataBlock, offset + 0x18, nameBytes, 0, nameLength); - name = new String(nameBytes, StandardCharsets.UTF_16LE).trim(); - } - } - } - - private class FileMetadata { - public int offset; - public int parentDirectoryOffset; - public int siblingFileOffset; - public long fileDataOffset; - public long fileDataLength; - public int nextFileInHashBucketOffset; - public int nameLength; - public String name; - public RomfsFile file; // used only for rebuilding CXI - - public FileMetadata(byte[] fileMetadataBlock, int offset) { - this.offset = offset; - parentDirectoryOffset = FileFunctions.readFullInt(fileMetadataBlock, offset); - siblingFileOffset = FileFunctions.readFullInt(fileMetadataBlock, offset + 0x04); - fileDataOffset = FileFunctions.readFullLong(fileMetadataBlock, offset + 0x08); - fileDataLength = FileFunctions.readFullLong(fileMetadataBlock, offset + 0x10); - nextFileInHashBucketOffset = FileFunctions.readFullInt(fileMetadataBlock, offset + 0x18); - nameLength = FileFunctions.readFullInt(fileMetadataBlock, offset + 0x1C); - name = ""; - if (nameLength != metadata_unused) { - byte[] nameBytes = new byte[nameLength]; - System.arraycopy(fileMetadataBlock, offset + 0x20, nameBytes, 0, nameLength); - name = new String(nameBytes, StandardCharsets.UTF_16LE).trim(); - } - } - - public byte[] asBytes() { - int metadataLength = 0x20; - if (nameLength != metadata_unused) { - metadataLength += alignInt(nameLength, 4); - } - byte[] output = new byte[metadataLength]; - FileFunctions.writeFullInt(output, 0x00, this.parentDirectoryOffset); - FileFunctions.writeFullInt(output, 0x04, this.siblingFileOffset); - FileFunctions.writeFullLong(output, 0x08, this.fileDataOffset); - FileFunctions.writeFullLong(output, 0x10, this.fileDataLength); - FileFunctions.writeFullInt(output, 0x18, this.nextFileInHashBucketOffset); - FileFunctions.writeFullInt(output, 0x1C, this.nameLength); - if (!name.equals("")) { - byte[] nameBytes = name.getBytes(StandardCharsets.UTF_16LE); - System.arraycopy(nameBytes, 0, output, 0x20, nameBytes.length); - } - return output; - } - } -} |