From 544a827a5e3609b1fc31402d7b7f9b656b2247b8 Mon Sep 17 00:00:00 2001 From: Matthew Sanders Date: Sun, 16 Nov 2025 00:06:25 -0700 Subject: [PATCH] I fixed file extraction by adding a `--file` command line parameter. --- BUILDING.md | 35 +++++++++++++++---- README.md | 76 +++++++++++++++++++++++++--------------- src/FsProcess.cpp | 33 +++++++++++------- src/FsProcess.h | 4 ++- src/GameCardProcess.cpp | 24 ++++++++----- src/GameCardProcess.h | 6 ++-- src/NcaProcess.cpp | 24 ++++++++----- src/NcaProcess.h | 4 ++- src/PfsProcess.cpp | 17 +++++++-- src/PfsProcess.h | 4 ++- src/RomfsProcess.cpp | 8 ++++- src/RomfsProcess.h | 2 ++ src/Settings.cpp | 77 ++++++++++++++++++++++++++++++----------- src/Settings.h | 9 ++++- src/main.cpp | 21 +++++------ 15 files changed, 240 insertions(+), 104 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index e72835f..81f6cfa 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,31 +1,52 @@ # Building + ## Git Submodules + This project makes use of git submodules to import dependencies into the source tree. After cloning this repository using git, prior to building NSTool the dependencies need to be downloaded. Run these two commands to initialise and download the dependencies: -``` + +```bash git submodule init git submodule update ``` -## Linux (incl. Windows Subsystem for Linux) & MacOS - Makefile +## Linux (including Windows Subsystem for Linux) & MacOS - Makefile + ### Requirements + * `make` * Terminal access * Typical GNU compatible development tools (e.g. `clang`, `g++`, `c++`, `ar` etc) with __C++11__ support ### Using Makefile + * `make` (default) - Compile program - * Compiling the program requires local dependencies to be compiled via `make deps` beforehand +* Compiling the program requires local dependencies to be compiled via `make deps` beforehand * `make clean` - Remove executable and object files * `make deps` - Compile locally included dependency libraries * `make clean_deps` - Remove compiled library binaries and object files ## Native Windows - Visual Studio -### Requirements -* [Visual Studio Community](https://visualstudio.microsoft.com/vs/community/) 2015 / 2017 / 2019 -### Compiling NSTool +### Requirements + +* [Visual Studio Community](https://visualstudio.microsoft.com/vs/community/) 2015 / 2017 / 2019 / 2022 + +### Compiling using the Visual Studio GUI + * Open `build/visualstudio/nstool.sln` in Visual Studio * Select Target (e.g `Debug`|`Release` & `x86`|`x64`) -* Navigate to `Build`->`Build Solution` \ No newline at end of file +* Navigate to `Build`->`Build Solution` + +### Compiling using the command line + +* Open PowerShell +* Paste and run the following command after double-checking the paths. + +```bash +& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" ` + "{full path to the cloned repository}\nstool\build\visualstudio\nstool.sln" ` + /p:Platform=x64 ` + /p:Configuration=Release +``` diff --git a/README.md b/README.md index d83a29a..e12e1c6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Nintendo Switch Tool (NSTool) ![DeviceTag](https://img.shields.io/badge/Device-SWITCH-e60012.svg) + General purpose reading/extraction tool for Nintendo Switch file formats. ## Supported File Formats + * PartitionFs (`PFS0`) (.pfs0) * Sha256PartitionFs (`HFS0`) (.hfs0) * RomFs (.romfs) @@ -10,47 +12,58 @@ General purpose reading/extraction tool for Nintendo Switch file formats. * NX GameCard Image (.xci) * Meta (`META`) (.npdm) * Nintendo Application Control Property (.nacp) -* Content Metadata (.cnmt) +* Content Metadata (.cnmt) * ES Certificate (.cert) * ES Ticket (v2 only) (.tik) -* Nintendo Shared Object (`NSO0`) (.nso) +* Nintendo Shared Object (`NSO0`) (.nso) * Nintendo Relocatable Object (`NRO0`) (.nro) * Initial Program Bundle (`INI1`) (.ini) * Initial Program (`KIP1`) (.kip) # Usage + ## General usage + The default mode of NSTool is to show general information about a file. To display general information the usage is as follows: -``` + +```bash nstool some_file.bin ``` However not all information is shown in this mode; file-layout, key data and properties set to default values are omitted. ## Alternative output modes + To output file-layout information, use the `--showlayout` option: -``` + +```bash nstool --showlayout some_file.bin ``` To output key data generation and selection, use the `--showkeys` option: -``` + +```bash nstool --showkeys some_file.bin ``` To output all information, enable the verbose output mode with the `-v` or `--verbose` option: -``` + +```bash nstool -v some_file.bin ``` ## Specify File Type + NSTool will in most cases correctly identify the file type. However you can override this and manually specify the file type with the `-t` or `--type` option: -``` + +```bash nstool -t cnmt some_file.bin ``` + In that example `cnmt` was selected, NSTool would process the file as `Content Metadata`. See below for a list of supported file type codes: + | Code | Description | | ----------- | --------------- | | gc, xci | NX GameCard Image | @@ -71,14 +84,17 @@ In that example `cnmt` was selected, NSTool would process the file as `Content M | aset, asset | Homebrew NRO Asset Binary | ## Validate Input File + Some file types have signatures/hashes/fields that can be validated by NSTool, but this mode isn't enabled by default. To validate files with NSTool, enable the verify mode with the `-y` or `--verify` option: -``` + +```bash nstool -y some_file.bin ``` See the below table for file types that support optional validation: + | File Type | Validation | Comments | | --------- | ---------- | -------- | | ES Certificate | Signature | If certificate is part of a certificate chain it will validate it as part of that chain. `Root` signed certificates are verified with user supplied `Root` public key. | @@ -91,17 +107,21 @@ See the below table for file types that support optional validation: * As of NSTool v1.6.0 the public key(s) for `Root Certificate`, `XCI Header`, `ACID` and `NCA Header` are built-in, and will be used if the user does not supply the public key in a key file. ## DevKit Mode + Files generated for `Production` use different (for the most part) encryption/signing keys than files generated for `Development`. NSTool will select `Production` encryption/signing keys by default. When handling files intended for developer consoles (e.g. systemupdaters, devtools, test builds, etc), you should enable developer mode with the `-d`, `--dev` option: -``` + +```bash nstool -d some_file.bin ``` ## Extract Files + Some file types have an internal file system. This can be displayed and extracted. To display the file system tree, use the file tree option `--fstree`: -``` + +```bash nstool --fstree some_file.bin ``` @@ -110,32 +130,29 @@ To extract the file system, use the extract option `-x`, `--extract`. Which has 1) Extract the entire file system. This extracts the contents of the entire file system to `./extract_dir/`. `extract_dir` will be created if it doesn't exist. -``` -nstool -x ./extract_dir/ some_file.bin + +```bash +nstool --extract ./extract_dir/ some_file.bin ``` 2) Extract a sub directory. This extracts the contents of `/a/sub/directory/` to `./extract_dir/`. `extract_dir` will be created if it doesn't exist. -``` -nstool -x /a/sub/directory/ ./extract_dir/ some_file.bin + +```bash +nstool --extract /a/sub/directory/ ./extract_dir/ some_file.bin ``` -3) Extract a specific file, preserving the original name. +3) Extract a specific file while preserving the original name. This extracts `/path/to/a/file.bin` to `./extract_dir/file.bin`. -``` -nstool -x /path/to/a/file.bin ./extract_dir/ some_file.bin -``` -4) Extract a specific file with a custom name. - -This extracts `/path/to/a/file.bin` to `./extract_dir/different_name.bin`. -``` -nstool -x /path/to/a/file.bin ./extract_dir/different_name.bin some_file.bin +```bash +nstool --extract ./extract_dir/ --file filename.bin some_file.bin ``` ### Supported File Types + * PartitionFs * Sha256PartitionFs * RomFs (including RomFs embedded in Homebrew NRO) @@ -144,24 +161,29 @@ nstool -x /path/to/a/file.bin ./extract_dir/different_name.bin some_file.bin * XCI ## NCA Patches + Nintendo distributes game patches/updates in the style of a diff to keep file sizes down. This means extracting game patches requires the base version of the game to be able to process patch data. Typically this is only done for the Program NCA. If `basegame_v0.nca` is the base Program NCA, and `gamepatch_v13219.nca` is the patch Program NCA, simply specify the base NCA using the base NCA option `--basenca` when processing the patch NCA. -``` +```bash nstool --basenca ./basegame_v0.nca -x ./patchdata gamepatch_v13219.nca ``` + In the above example the patch NCA is being extracted to `./patchdata` ## Encrypted Files -Some Nintendo Switch files are partially or completely encrypted. These require the user to supply the encryption keys to NSTool so that it can process them. + +Some Nintendo Switch files are partially or completely encrypted. These require the user to supply the encryption keys to NSTool so that it can process them. See [SWITCH_KEYS.md](/SWITCH_KEYS.md) for more info. # External Keys -NSTool doesn't embed any keys that are copyright protected. However keys can be imported via various keyset files. + +NSTool doesn't embed any keys that are copyright protected. However keys can be imported via various keyset files. See [SWITCH_KEYS.md](/SWITCH_KEYS.md) for more info. # Building -See [BUILDING.md](/BUILDING.md). \ No newline at end of file + +See [BUILDING.md](/BUILDING.md). diff --git a/src/FsProcess.cpp b/src/FsProcess.cpp index ade3c21..58f79cb 100644 --- a/src/FsProcess.cpp +++ b/src/FsProcess.cpp @@ -12,6 +12,7 @@ nstool::FsProcess::FsProcess() : mShowFsInfo(false), mProperties(), mShowFsTree(false), + mOutputFile(), mFsRootLabel(), mExtractJobs(), mDataCache(0x10000) @@ -39,7 +40,7 @@ void nstool::FsProcess::process() { printFs(); } - + if (mExtractJobs.empty() == false) { extractFs(); @@ -81,6 +82,11 @@ void nstool::FsProcess::setExtractJobs(const std::vector& ex mExtractJobs = extract_jobs; } +void nstool::FsProcess::setExtractFile(std::string outputFile) +{ + mOutputFile = outputFile; +} + void nstool::FsProcess::printFs() { fmt::print("[{:s}/Tree]\n", (mFsFormatName.isSet() ? mFsFormatName.get() : "FileSystem")); @@ -124,7 +130,7 @@ void nstool::FsProcess::extractFs() tc::io::Path file_extract_path = itr->extract_path + itr->virtual_path.back(); - fmt::print("Saving {:s}...\n", file_extract_path.to_string()); + fmt::print("Extracting file to {:s}...\n", file_extract_path.to_string()); writeStreamToFile(file_stream, itr->extract_path + itr->virtual_path.back(), mDataCache); @@ -142,7 +148,7 @@ void nstool::FsProcess::extractFs() // get path to parent directory tc::io::Path parent_dir_path = itr->extract_path; - // replace final path element with the current directory alias + // replace final path element with the current directory alias parent_dir_path.pop_back(); // remove filename parent_dir_path.push_back("."); // replace with the current dir name alias @@ -185,7 +191,7 @@ void nstool::FsProcess::extractFs() fmt::print("[WARNING] Failed to extract virtual path: \"{:s}\"\n", itr->virtual_path.to_string()); } - + } void nstool::FsProcess::visitDir(const tc::io::Path& v_path, const tc::io::Path& l_path, bool extract_fs, bool print_fs) @@ -216,17 +222,17 @@ void nstool::FsProcess::visitDir(const tc::io::Path& v_path, const tc::io::Path& std::shared_ptr out_stream; for (auto itr = info.file_list.begin(); itr != info.file_list.end(); itr++) { + // build out path + out_path = l_path + *itr; + if (print_fs) { for (size_t i = 0; i < v_path.size(); i++) fmt::print(" "); fmt::print(" {:s}\n", *itr); } - if (extract_fs) + if (extract_fs && (mOutputFile == "" || (mOutputFile == *itr))) { - // build out path - out_path = l_path + *itr; - fmt::print("Saving {:s}...\n", out_path.to_string()); // begin export @@ -240,7 +246,7 @@ void nstool::FsProcess::visitDir(const tc::io::Path& v_path, const tc::io::Path& cache_read_len = in_stream->read(mDataCache.data(), mDataCache.size()); if (cache_read_len == 0) { - throw tc::io::IOException(mModuleLabel, fmt::format("Failed to read from {:s}file.", (mFsFormatName.isSet() ? (mFsFormatName.get() + " ") : ""))); + throw tc::io::IOException(mModuleLabel, fmt::format("Failed to read from {:s} file.", (mFsFormatName.isSet() ? (mFsFormatName.get() + " ") : ""))); } out_stream->write(mDataCache.data(), cache_read_len); @@ -250,9 +256,12 @@ void nstool::FsProcess::visitDir(const tc::io::Path& v_path, const tc::io::Path& } } - // iterate thru child dirs + // iterate through child directories for (auto itr = info.dir_list.begin(); itr != info.dir_list.end(); itr++) { - visitDir(v_path + *itr, l_path + *itr, extract_fs, print_fs); + // When traversing each directory append the directory to the local path only if we're not looking to extract a single file. + const tc::io::Path localPath = mOutputFile == "" ? l_path + *itr : l_path; + + visitDir(v_path + *itr, localPath, extract_fs, print_fs); } -} \ No newline at end of file +} diff --git a/src/FsProcess.h b/src/FsProcess.h index ac11c16..9c3f283 100644 --- a/src/FsProcess.h +++ b/src/FsProcess.h @@ -21,6 +21,7 @@ public: void setShowFsTree(bool show_fs_tree); void setFsRootLabel(const std::string& root_label); void setExtractJobs(const std::vector& extract_jobs); + void setExtractFile(std::string outputFile); private: std::string mModuleLabel; @@ -37,10 +38,11 @@ private: // extract jobs std::vector mExtractJobs; + std::string mOutputFile; // cache for file extract tc::ByteData mDataCache; - + void printFs(); void extractFs(); diff --git a/src/GameCardProcess.cpp b/src/GameCardProcess.cpp index a1c08c4..fc73394 100644 --- a/src/GameCardProcess.cpp +++ b/src/GameCardProcess.cpp @@ -46,6 +46,11 @@ void nstool::GameCardProcess::setInputFile(const std::shared_ptr extract_jobs) { mFsProcess.setExtractJobs(extract_jobs); + mFsProcess.setExtractFile(mOutputFile); } void nstool::GameCardProcess::importHeader() @@ -81,7 +87,7 @@ void nstool::GameCardProcess::importHeader() { throw tc::NotSupportedException(mModuleName, "Input stream requires read/seek permissions."); } - + // check stream is large enough for header if (mFile->length() < tc::io::IOUtil::castSizeToInt64(sizeof(pie::hac::sSdkGcHeader))) { @@ -106,7 +112,7 @@ void nstool::GameCardProcess::importHeader() mIsTrueSdkXci = false; mGcHeaderOffset = 0; } - else + else { throw tc::Exception(mModuleName, "Corrupt GameCard Image: Unexpected magic bytes."); } @@ -115,10 +121,10 @@ void nstool::GameCardProcess::importHeader() // generate hash of raw header tc::crypto::GenerateSha2256Hash(mHdrHash.data(), (byte_t*)&hdr_ptr->header, sizeof(pie::hac::sGcHeader)); - + // save the signature memcpy(mHdrSignature.data(), hdr_ptr->signature.data(), mHdrSignature.size()); - + // decrypt extended header byte_t xci_header_key_index = hdr_ptr->header.key_flag & 0xf; if (mKeyCfg.xci_header_key.find(xci_header_key_index) != mKeyCfg.xci_header_key.end()) @@ -126,7 +132,7 @@ void nstool::GameCardProcess::importHeader() pie::hac::GameCardUtil::decryptXciHeader(&hdr_ptr->header, mKeyCfg.xci_header_key[xci_header_key_index].data()); mProccessExtendedHeader = true; } - + // deserialise header mHdr.fromBytes((byte_t*)&hdr_ptr->header, sizeof(pie::hac::sGcHeader)); } @@ -147,8 +153,8 @@ void nstool::GameCardProcess::displayHeader() { fmt::print(" {:s}\n", pie::hac::GameCardUtil::getHeaderFlagsAsString((pie::hac::gc::HeaderFlags)*itr)); } - - + + if (mCliOutputMode.show_extended_info) { fmt::print(" KekIndex: {:s} ({:d})\n", pie::hac::GameCardUtil::getKekIndexAsString((pie::hac::gc::KekIndex)mHdr.getKekIndex()), mHdr.getKekIndex()); @@ -197,7 +203,7 @@ void nstool::GameCardProcess::displayHeader() } } - + if (mProccessExtendedHeader) { fmt::print("[GameCard/ExtendedHeader]\n"); @@ -252,7 +258,7 @@ void nstool::GameCardProcess::validateXciSignature() fmt::print("[WARNING] GameCard Header Signature: FAIL\n"); } } - else + else { fmt::print("[WARNING] GameCard Header Signature: FAIL (Failed to load rsa public key.)\n"); } diff --git a/src/GameCardProcess.h b/src/GameCardProcess.h index b968282..405e604 100644 --- a/src/GameCardProcess.h +++ b/src/GameCardProcess.h @@ -16,6 +16,7 @@ public: // generic void setInputFile(const std::shared_ptr& file); + void setOutputFile(const std::string& file); void setKeyCfg(const KeyBag& keycfg); void setCliOutputMode(CliOutputMode type); void setVerifyMode(bool verify); @@ -29,10 +30,11 @@ private: std::string mModuleName; std::shared_ptr mFile; + std::string mOutputFile; KeyBag mKeyCfg; CliOutputMode mCliOutputMode; bool mVerify; - + bool mIsTrueSdkXci; bool mIsSdkXciEncrypted; size_t mGcHeaderOffset; @@ -40,7 +42,7 @@ private: pie::hac::detail::rsa2048_signature_t mHdrSignature; pie::hac::detail::sha256_hash_t mHdrHash; pie::hac::GameCardHeader mHdr; - + // fs processing std::shared_ptr mFileSystem; FsProcess mFsProcess; diff --git a/src/NcaProcess.cpp b/src/NcaProcess.cpp index 1c0dcfb..32735f3 100644 --- a/src/NcaProcess.cpp +++ b/src/NcaProcess.cpp @@ -49,6 +49,11 @@ void nstool::NcaProcess::setInputFile(const std::shared_ptr& fi mFile = file; } +void nstool::NcaProcess::setOutputFile(const std::string& file) +{ + mOutputFile = file; +} + void nstool::NcaProcess::setBaseNcaPath(const tc::Optional& nca_path) { mBaseNcaPath = nca_path; @@ -82,6 +87,7 @@ void nstool::NcaProcess::setFsRootLabel(const std::string& root_label) void nstool::NcaProcess::setExtractJobs(const std::vector& extract_jobs) { mFsProcess.setExtractJobs(extract_jobs); + mFsProcess.setExtractFile(mOutputFile); } const std::shared_ptr& nstool::NcaProcess::getFileSystem() const @@ -127,7 +133,7 @@ void nstool::NcaProcess::generateNcaBodyEncryptionKeys() // create zeros key KeyBag::aes128_key_t zero_aesctr_key; memset(zero_aesctr_key.data(), 0, zero_aesctr_key.size()); - + // get key data from header byte_t masterkey_rev = pie::hac::AesKeygen::getMasterKeyRevisionFromKeyGeneration(mHdr.getKeyGeneration()); byte_t keak_index = mHdr.getKeyAreaEncryptionKeyIndex(); @@ -215,7 +221,7 @@ void nstool::NcaProcess::generateNcaBodyEncryptionKeys() mContentKey.aes_ctr = mKeyCfg.fallback_content_key.get(); } } - + if (mCliOutputMode.show_keydata) { if (mContentKey.aes_ctr.isSet()) @@ -276,7 +282,7 @@ void nstool::NcaProcess::generatePartitionConfiguration() throw tc::Exception(mModuleName, fmt::format("NCA FS Header [{:d}] Version({:d}): UNSUPPORTED", partition.header_index, fs_header.version.unwrap())); } - // setup AES-CTR + // setup AES-CTR pie::hac::ContentArchiveUtil::getNcaPartitionAesCtr(&fs_header, info.aes_ctr.data()); // save partition configinfo @@ -290,14 +296,14 @@ void nstool::NcaProcess::generatePartitionConfiguration() if (info.hash_type == pie::hac::nca::HashType_HierarchicalSha256) { info.hierarchicalsha256_hdr.fromBytes(fs_header.hash_info.data(), fs_header.hash_info.size()); - } + } else if (info.hash_type == pie::hac::nca::HashType_HierarchicalIntegrity) { info.hierarchicalintegrity_hdr.fromBytes(fs_header.hash_info.data(), fs_header.hash_info.size()); } // create reader - try + try { // handle partition encryption and partition compaction (sparse layer) if (fs_header.sparse_info.generation.unwrap() != 0) @@ -430,7 +436,7 @@ void nstool::NcaProcess::validateNcaSignatures() { fmt::print("[WARNING] NCA Header Main Signature: FAIL (could not load header key)\n"); } - + // validate signature[1] if (mHdr.getContentType() == pie::hac::nca::ContentType_Program) @@ -493,7 +499,7 @@ void nstool::NcaProcess::displayHeader() { fmt::print(" RightsId: {:s}\n", tc::cli::FormatUtil::formatBytesAsString(mHdr.getRightsId().data(), mHdr.getRightsId().size(), true, "")); } - + if (mContentKey.kak_list.size() > 0 && mCliOutputMode.show_keydata) { fmt::print(" Key Area:\n"); @@ -504,9 +510,9 @@ void nstool::NcaProcess::displayHeader() { std::string enc_key = tc::cli::FormatUtil::formatBytesAsString(mContentKey.kak_list[i].enc.data(), mContentKey.kak_list[i].enc.size(), true, ""); std::string dec_key = mContentKey.kak_list[i].decrypted ? tc::cli::FormatUtil::formatBytesAsString(mContentKey.kak_list[i].dec.data(), mContentKey.kak_list[i].dec.size(), true, "") : ""; - + fmt::print(" | {:3d} | {:32s} | {:32s} |\n", mContentKey.kak_list[i].index, enc_key, dec_key); - + } fmt::print(" <--------------------------------------------------------------------------->\n"); } diff --git a/src/NcaProcess.h b/src/NcaProcess.h index 9a529c8..2fff43d 100644 --- a/src/NcaProcess.h +++ b/src/NcaProcess.h @@ -18,6 +18,7 @@ public: // generic void setInputFile(const std::shared_ptr& file); + void setOutputFile(const std::string& file); void setKeyCfg(const KeyBag& keycfg); void setCliOutputMode(CliOutputMode type); void setVerifyMode(bool verify); @@ -38,6 +39,7 @@ private: // user options std::shared_ptr mFile; + std::string mOutputFile; KeyBag mKeyCfg; CliOutputMode mCliOutputMode; bool mVerify; @@ -121,7 +123,7 @@ private: // sparse metadata SparseInfo sparse_info; }; - + std::array mPartitions; void importHeader(); diff --git a/src/PfsProcess.cpp b/src/PfsProcess.cpp index d156e25..eddc797 100644 --- a/src/PfsProcess.cpp +++ b/src/PfsProcess.cpp @@ -67,10 +67,10 @@ void nstool::PfsProcess::process() // set properties for FsProcess mFsProcess.setFsProperties({ - fmt::format("Type: {:s}", pie::hac::PartitionFsUtil::getFsTypeAsString(mPfs.getFsType())), + fmt::format("Type: {:s}", pie::hac::PartitionFsUtil::getFsTypeAsString(mPfs.getFsType())), fmt::format("FileNum: {:d}", mPfs.getFileList().size()) }); - + mFsProcess.process(); } @@ -79,6 +79,11 @@ void nstool::PfsProcess::setInputFile(const std::shared_ptr& fi mFile = file; } +void nstool::PfsProcess::setOutputFile(const std::string& file) +{ + mOutputFile = file; +} + void nstool::PfsProcess::setCliOutputMode(CliOutputMode type) { mCliOutputMode = type; @@ -103,6 +108,7 @@ void nstool::PfsProcess::setFsRootLabel(const std::string& root_label) void nstool::PfsProcess::setExtractJobs(const std::vector& extract_jobs) { mFsProcess.setExtractJobs(extract_jobs); + mFsProcess.setExtractFile(mOutputFile); } const pie::hac::PartitionFsHeader& nstool::PfsProcess::getPfsHeader() const @@ -118,6 +124,13 @@ const std::shared_ptr& nstool::PfsProcess::getFileSystem() size_t nstool::PfsProcess::determineHeaderSize(const pie::hac::sPfsHeader* hdr) { size_t fileEntrySize = 0; + + if (hdr->st_magic.unwrap() == pie::hac::pfs::kPfsStructMagic) { + fileEntrySize = sizeof(pie::hac::sPfsFile); + } else { + fileEntrySize = sizeof(pie::hac::sHashedPfsFile); + } + if (hdr->st_magic.unwrap() == pie::hac::pfs::kPfsStructMagic) fileEntrySize = sizeof(pie::hac::sPfsFile); else diff --git a/src/PfsProcess.h b/src/PfsProcess.h index ee374f2..656c72b 100644 --- a/src/PfsProcess.h +++ b/src/PfsProcess.h @@ -15,6 +15,7 @@ public: // generic void setInputFile(const std::shared_ptr& file); + void setOutputFile(const std::string& file); void setCliOutputMode(CliOutputMode type); void setVerifyMode(bool verify); @@ -33,6 +34,7 @@ private: std::string mModuleName; std::shared_ptr mFile; + std::string mOutputFile; CliOutputMode mCliOutputMode; bool mVerify; @@ -40,7 +42,7 @@ private: std::shared_ptr mFileSystem; FsProcess mFsProcess; - + size_t determineHeaderSize(const pie::hac::sPfsHeader* hdr); bool validateHeaderMagic(const pie::hac::sPfsHeader* hdr); }; diff --git a/src/RomfsProcess.cpp b/src/RomfsProcess.cpp index ccc5806..22c1a2a 100644 --- a/src/RomfsProcess.cpp +++ b/src/RomfsProcess.cpp @@ -113,7 +113,7 @@ void nstool::RomfsProcess::process() // set properties for FsProcess mFsProcess.setFsProperties({ - fmt::format("DirNum: {:d}", mDirNum), + fmt::format("DirNum: {:d}", mDirNum), fmt::format("FileNum: {:d}", mFileNum) }); @@ -126,6 +126,11 @@ void nstool::RomfsProcess::setInputFile(const std::shared_ptr& mFile = file; } +void nstool::RomfsProcess::setOutputFile(const std::string& file) +{ + mOutputFile = file; +} + void nstool::RomfsProcess::setCliOutputMode(CliOutputMode type) { mCliOutputMode = type; @@ -145,6 +150,7 @@ void nstool::RomfsProcess::setFsRootLabel(const std::string& root_label) void nstool::RomfsProcess::setExtractJobs(const std::vector& extract_jobs) { mFsProcess.setExtractJobs(extract_jobs); + mFsProcess.setExtractFile(mOutputFile); } void nstool::RomfsProcess::setShowFsTree(bool list_fs) diff --git a/src/RomfsProcess.h b/src/RomfsProcess.h index d483900..952a32d 100644 --- a/src/RomfsProcess.h +++ b/src/RomfsProcess.h @@ -15,6 +15,7 @@ public: // generic void setInputFile(const std::shared_ptr& file); + void setOutputFile(const std::string& file); void setCliOutputMode(CliOutputMode type); void setVerifyMode(bool verify); @@ -28,6 +29,7 @@ private: std::string mModuleName; std::shared_ptr mFile; + std::string mOutputFile; CliOutputMode mCliOutputMode; bool mVerify; diff --git a/src/Settings.cpp b/src/Settings.cpp index 46d5d13..16cf36e 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -55,7 +55,7 @@ private: class DeprecatedOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - DeprecatedOptionHandler(const std::string& warn_message, const std::vector& opts) : + DeprecatedOptionHandler(const std::string& warn_message, const std::vector& opts) : mWarnMessage(warn_message), mOptStrings(opts), mOptRegex() @@ -84,7 +84,7 @@ private: class FlagOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - FlagOptionHandler(bool& flag, const std::vector& opts) : + FlagOptionHandler(bool& flag, const std::vector& opts) : mFlag(flag), mOptStrings(opts), mOptRegex() @@ -118,7 +118,7 @@ private: class SingleParamStringOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - SingleParamStringOptionHandler(tc::Optional& param, const std::vector& opts) : + SingleParamStringOptionHandler(tc::Optional& param, const std::vector& opts) : mParam(param), mOptStrings(opts), mOptRegex() @@ -152,7 +152,7 @@ private: class SingleParamPathOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - SingleParamPathOptionHandler(tc::Optional& param, const std::vector& opts) : + SingleParamPathOptionHandler(tc::Optional& param, const std::vector& opts) : mParam(param), mOptStrings(opts), mOptRegex() @@ -186,7 +186,7 @@ private: class SingleParamSizetOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - SingleParamSizetOptionHandler(size_t& param, const std::vector& opts) : + SingleParamSizetOptionHandler(size_t& param, const std::vector& opts) : mParam(param), mOptStrings(opts), mOptRegex() @@ -263,7 +263,7 @@ private: class SingleParamPathArrayOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - SingleParamPathArrayOptionHandler(std::vector& param, const std::vector& opts) : + SingleParamPathArrayOptionHandler(std::vector& param, const std::vector& opts) : mParam(param), mOptStrings(opts), mOptRegex() @@ -297,7 +297,7 @@ private: class FileTypeOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - FileTypeOptionHandler(nstool::Settings::FileType& param, const std::vector& opts) : + FileTypeOptionHandler(nstool::Settings::FileType& param, const std::vector& opts) : mParam(param), mOptStrings(opts), mOptRegex() @@ -397,6 +397,40 @@ private: std::vector mOptRegex; }; +class SingleFileExtractionHandler : public tc::cli::OptionParser::IOptionHandler +{ +public: + SingleFileExtractionHandler(std::string& param, const std::vector& opts) : + mParam(param), + mOptStrings(opts), + mOptRegex() + {} + + const std::vector& getOptionStrings() const + { + return mOptStrings; + } + + const std::vector& getOptionRegexPatterns() const + { + return mOptRegex; + } + + void processOption(const std::string& option, const std::vector& params) + { + if (params.size() != 1) + { + throw tc::ArgumentOutOfRangeException(fmt::format("Option \"{:s}\" requires a parameter. Give the name and extension of the file you want to extract.", option)); + } + + mParam = params[0]; + } +private: + std::string& mParam; + std::vector mOptStrings; + std::vector mOptRegex; +}; + class InstructionTypeOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: @@ -445,7 +479,7 @@ private: class ExtractDataPathOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - ExtractDataPathOptionHandler(std::vector& jobs, const std::vector& opts) : + ExtractDataPathOptionHandler(std::vector& jobs, const std::vector& opts) : mJobs(jobs), mOptStrings(opts), mOptRegex() @@ -470,7 +504,7 @@ public: else if (params.size() == 2) { mJobs.push_back({tc::io::Path(params[0]), tc::io::Path(params[1])}); - } + } else { throw tc::ArgumentOutOfRangeException(fmt::format("Option \"{:s}\" requires parameters in the format \"[] \".", option)); @@ -485,7 +519,7 @@ private: class CustomExtractDataPathOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: - CustomExtractDataPathOptionHandler(std::vector& jobs, const std::vector& opts, const tc::io::Path& custom_path) : + CustomExtractDataPathOptionHandler(std::vector& jobs, const std::vector& opts, const tc::io::Path& custom_path) : mJobs(jobs), mOptStrings(opts), mOptRegex(), @@ -519,7 +553,7 @@ public: { fmt::print("Consider using \"-x {:s} {:s}\" instead.\n", mCustomPath.to_string(), params[0]); } - + mJobs.push_back({mCustomPath, tc::io::Path(params[0])}); } @@ -604,7 +638,7 @@ void nstool::SettingsInitializer::parse_args(const std::vector& arg usage_text(); throw tc::ArgumentException(mModuleLabel, "Not enough arguments."); } - + // detect request for help for (auto itr = ++(args.begin()); itr != args.end(); itr++) { @@ -636,6 +670,7 @@ void nstool::SettingsInitializer::parse_args(const std::vector& arg // process input file type opts.registerOptionHandler(std::shared_ptr(new FileTypeOptionHandler(infile.filetype, { "-t", "--type" }))); + opts.registerOptionHandler(std::shared_ptr(new SingleFileExtractionHandler(outfile.filename, { "--file" }))); // get user-provided keydata opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(mKeysetPath, {"-k", "--keyset"}))); @@ -671,12 +706,12 @@ void nstool::SettingsInitializer::parse_args(const std::vector& arg // kip options opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(kip.extract_path, { "--kipdir" }))); - + // aset options opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(aset.icon_extract_path, { "--icon" }))); opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(aset.nacp_extract_path, { "--nacp" }))); - + // process option opts.processOptions(args, 1, args.size() - 2); } @@ -684,7 +719,7 @@ void nstool::SettingsInitializer::parse_args(const std::vector& arg void nstool::SettingsInitializer::determine_filetype() { //fmt::print("infile path = \"{}\"\n", infile.path.get().to_string()); - + auto file = tc::io::StreamSource(std::make_shared(tc::io::FileStream(infile.path.get(), tc::io::FileMode::Open, tc::io::FileAccess::Read))); auto raw_data = file.pullData(0, 0x5000); @@ -808,11 +843,11 @@ void nstool::SettingsInitializer::usage_text() const fmt::print(" --showlayout Show layout metadata.\n"); fmt::print(" -v, --verbose Verbose output.\n"); fmt::print("\n PFS0/HFS0 (PartitionFs), RomFs, NSP (Nintendo Submission Package)\n"); - fmt::print(" {:s} [--fstree] [-x [] ] \n", BIN_NAME); + fmt::print(" {:s} [--fstree] [-x --file ] \n", BIN_NAME); fmt::print(" --fstree Print filesystem tree.\n"); fmt::print(" -x, --extract Extract a file or directory to local filesystem.\n"); fmt::print("\n XCI (GameCard Image)\n"); - fmt::print(" {:s} [--fstree] [-x [] ] <.xci file>\n", BIN_NAME); + fmt::print(" {:s} [--fstree] [-x --file ] <.xci file>\n", BIN_NAME); fmt::print(" --fstree Print filesystem tree.\n"); fmt::print(" -x, --extract Extract a file or directory to local filesystem.\n"); fmt::print(" --update Extract \"update\" partition to directory. (Alias for \"-x /update \")\n"); @@ -820,7 +855,7 @@ void nstool::SettingsInitializer::usage_text() const fmt::print(" --normal Extract \"normal\" partition to directory. (Alias for \"-x /normal \")\n"); fmt::print(" --secure Extract \"secure\" partition to directory. (Alias for \"-x /secure \")\n"); fmt::print("\n NCA (Nintendo Content Archive)\n"); - fmt::print(" {:s} [--fstree] [-x [] ] [--bodykey --titlekey -tik --basenca <.nca file>] <.nca file>\n", BIN_NAME); + fmt::print(" {:s} [--fstree] [-x --file ] [--bodykey --titlekey -tik --basenca <.nca file>] <.nca file>\n", BIN_NAME); fmt::print(" --fstree Print filesystem tree.\n"); fmt::print(" -x, --extract Extract a file or directory to local filesystem.\n"); fmt::print(" --titlekey Specify (encrypted) title key extracted from ticket.\n"); @@ -1000,7 +1035,7 @@ void nstool::SettingsInitializer::loadKeyFile(tc::Optional& keyfil try { tc::io::FileStream test = tc::io::FileStream(tmp_path, tc::io::FileMode::Open, tc::io::FileAccess::Read); - + keyfile_path = tmp_path; } catch (tc::io::FileNotFoundException&) { @@ -1010,7 +1045,7 @@ void nstool::SettingsInitializer::loadKeyFile(tc::Optional& keyfil else { fmt::print("[WARNING] Failed to locate \"{}\" keyfile.{}\n", keyfile_name, cli_hint); } - + } @@ -1020,7 +1055,7 @@ bool nstool::SettingsInitializer::determineValidNcaFromSample(const tc::ByteData { return false; } - + if (opt.keybag.nca_header_key.isNull()) { fmt::print("[WARNING] Failed to load NCA Header Key.\n"); diff --git a/src/Settings.h b/src/Settings.h index 28dc0eb..b9ca9f2 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -37,6 +37,11 @@ struct Settings tc::Optional path; } infile; + struct OutputFileOptions + { + std::string filename; + } outfile; + struct Options { CliOutputMode cli_output_mode; @@ -54,7 +59,7 @@ struct Settings } code; // Generic FS options - struct FsOptions + struct FsOptions { bool show_fs_tree; std::vector extract_jobs; @@ -97,6 +102,8 @@ struct Settings infile.filetype = FILE_TYPE_ERROR; infile.path = tc::Optional(); + outfile.filename = ""; + opt.cli_output_mode = CliOutputMode(); opt.verify = false; opt.is_dev = false; diff --git a/src/main.cpp b/src/main.cpp index 03773dd..e336c1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,25 +21,25 @@ int umain(const std::vector& args, const std::vector& env) { - try + try { nstool::Settings set = nstool::SettingsInitializer(args); - + std::shared_ptr infile_stream = std::make_shared(tc::io::FileStream(set.infile.path.get(), tc::io::FileMode::Open, tc::io::FileAccess::Read)); if (set.infile.filetype == nstool::Settings::FILE_TYPE_GAMECARD) - { + { nstool::GameCardProcess obj; obj.setInputFile(infile_stream); - + obj.setOutputFile(set.outfile.filename); obj.setKeyCfg(set.opt.keybag); obj.setCliOutputMode(set.opt.cli_output_mode); obj.setVerifyMode(set.opt.verify); obj.setShowFsTree(set.fs.show_fs_tree); obj.setExtractJobs(set.fs.extract_jobs); - + obj.process(); } else if (set.infile.filetype == nstool::Settings::FILE_TYPE_PARTITIONFS || set.infile.filetype == nstool::Settings::FILE_TYPE_NSP) @@ -47,21 +47,21 @@ int umain(const std::vector& args, const std::vector& nstool::PfsProcess obj; obj.setInputFile(infile_stream); - + obj.setOutputFile(set.outfile.filename); obj.setCliOutputMode(set.opt.cli_output_mode); obj.setVerifyMode(set.opt.verify); obj.setShowFsTree(set.fs.show_fs_tree); obj.setExtractJobs(set.fs.extract_jobs); - + obj.process(); } - else if (set.infile.filetype == nstool::Settings::FILE_TYPE_ROMFS) { nstool::RomfsProcess obj; obj.setInputFile(infile_stream); + obj.setOutputFile(set.outfile.filename); obj.setCliOutputMode(set.opt.cli_output_mode); obj.setVerifyMode(set.opt.verify); @@ -75,6 +75,7 @@ int umain(const std::vector& args, const std::vector& nstool::NcaProcess obj; obj.setInputFile(infile_stream); + obj.setOutputFile(set.outfile.filename); obj.setBaseNcaPath(set.nca.base_nca_path); obj.setKeyCfg(set.opt.keybag); obj.setCliOutputMode(set.opt.cli_output_mode); @@ -113,7 +114,7 @@ int umain(const std::vector& args, const std::vector& obj.setInputFile(infile_stream); obj.setCliOutputMode(set.opt.cli_output_mode); obj.setVerifyMode(set.opt.verify); - + obj.setIs64BitInstruction(set.code.is_64bit_instruction); obj.setListApi(set.code.list_api); obj.setListSymbols(set.code.list_symbols); @@ -127,7 +128,7 @@ int umain(const std::vector& args, const std::vector& obj.setInputFile(infile_stream); obj.setCliOutputMode(set.opt.cli_output_mode); obj.setVerifyMode(set.opt.verify); - + obj.setIs64BitInstruction(set.code.is_64bit_instruction); obj.setListApi(set.code.list_api); obj.setListSymbols(set.code.list_symbols);