mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 15:21:47 +01:00
Implement an NCA parser and loader
Nintendo Content Archives are used to store the assets, executables and updates of applications. They support holding either a PFS0 or a RomFS. An NCA's ExeFS can be loaded by placing each NSO sequentially into memory, starting with rtld which will link them together. Currently only decrypted NCAs are supported, encryption and BKTR handling will be added at a later time.
This commit is contained in:
parent
db64f53cfb
commit
a5513bd7e6
@ -43,6 +43,7 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/loader/loader.cpp
|
||||
${source_DIR}/skyline/loader/nro.cpp
|
||||
${source_DIR}/skyline/loader/nso.cpp
|
||||
${source_DIR}/skyline/loader/nca.cpp
|
||||
${source_DIR}/skyline/kernel/memory.cpp
|
||||
${source_DIR}/skyline/kernel/ipc.cpp
|
||||
${source_DIR}/skyline/kernel/svc.cpp
|
||||
@ -104,6 +105,7 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/vfs/rom_filesystem.cpp
|
||||
${source_DIR}/skyline/vfs/os_backing.cpp
|
||||
${source_DIR}/skyline/vfs/nacp.cpp
|
||||
${source_DIR}/skyline/vfs/nca.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static)
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "skyline/vfs/os_backing.h"
|
||||
#include "skyline/loader/nro.h"
|
||||
#include "skyline/loader/nso.h"
|
||||
#include "skyline/loader/nca.h"
|
||||
#include "skyline/jvm.h"
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JNIEnv *env, jobject thiz, jint jformat, jint fd) {
|
||||
@ -17,6 +18,8 @@ extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JN
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NroLoader(backing));
|
||||
case skyline::loader::RomFormat::NSO:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NsoLoader(backing));
|
||||
case skyline::loader::RomFormat::NCA:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NcaLoader(backing));
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@ -52,4 +55,4 @@ extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicat
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_emu_skyline_loader_RomFile_destroy(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
delete reinterpret_cast<skyline::loader::NroLoader *>(instance);
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +213,7 @@ namespace skyline {
|
||||
namespace loader {
|
||||
class NroLoader;
|
||||
class NsoLoader;
|
||||
class NcaLoader;
|
||||
}
|
||||
|
||||
namespace kernel {
|
||||
@ -324,6 +325,7 @@ namespace skyline {
|
||||
friend class type::KProcess;
|
||||
friend class loader::NroLoader;
|
||||
friend class loader::NsoLoader;
|
||||
friend class loader::NcaLoader;
|
||||
|
||||
friend void svc::SetMemoryAttribute(DeviceState &state);
|
||||
|
||||
|
@ -15,6 +15,7 @@ namespace skyline::loader {
|
||||
enum class RomFormat {
|
||||
NRO, //!< The NRO format: https://switchbrew.org/wiki/NRO
|
||||
NSO, //!< The NSO format: https://switchbrew.org/wiki/NSO
|
||||
NCA, //!< The NCA format: https://switchbrew.org/wiki/NCA
|
||||
XCI, //!< The XCI format: https://switchbrew.org/wiki/XCI
|
||||
NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws
|
||||
};
|
||||
|
47
app/src/main/cpp/skyline/loader/nca.cpp
Normal file
47
app/src/main/cpp/skyline/loader/nca.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <nce.h>
|
||||
#include <os.h>
|
||||
#include <kernel/memory.h>
|
||||
#include "nso.h"
|
||||
#include "nca.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
NcaLoader::NcaLoader(const std::shared_ptr<vfs::Backing> &backing) : nca(backing) {
|
||||
if (nca.exeFs == nullptr)
|
||||
throw exception("Only NCAs with an ExeFS can be loaded directly");
|
||||
}
|
||||
|
||||
void NcaLoader::LoadExeFs(const std::shared_ptr<vfs::FileSystem> &exeFs, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state) {
|
||||
if (exeFs == nullptr)
|
||||
throw exception("Cannot load a null ExeFS");
|
||||
|
||||
auto nsoFile = exeFs->OpenFile("rtld");
|
||||
if (nsoFile == nullptr)
|
||||
throw exception("Cannot load an ExeFS that doesn't contain rtld");
|
||||
|
||||
auto loadInfo = NsoLoader::LoadNso(nsoFile, process, state);
|
||||
u64 offset = loadInfo.size;
|
||||
u64 base = loadInfo.base;
|
||||
|
||||
state.logger->Info("Loaded nso 'rtld' at 0x{:X}", base);
|
||||
|
||||
for (const auto &nso : {"main", "subsdk0", "subsdk1", "subsdk2", "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "sdk"}) {
|
||||
nsoFile = exeFs->OpenFile(nso);
|
||||
|
||||
if (nsoFile == nullptr)
|
||||
continue;
|
||||
|
||||
loadInfo = NsoLoader::LoadNso(nsoFile, process, state, offset);
|
||||
state.logger->Info("Loaded nso '{}' at 0x{:X}", nso, base + offset);
|
||||
offset += loadInfo.size;
|
||||
}
|
||||
|
||||
state.os->memory.InitializeRegions(base, offset, memory::AddressSpaceType::AddressSpace39Bit);
|
||||
}
|
||||
|
||||
void NcaLoader::LoadProcessData(const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state) {
|
||||
LoadExeFs(nca.exeFs, process, state);
|
||||
}
|
||||
}
|
30
app/src/main/cpp/skyline/loader/nca.h
Normal file
30
app/src/main/cpp/skyline/loader/nca.h
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <common.h>
|
||||
#include <vfs/nca.h>
|
||||
#include "loader.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
/**
|
||||
* @brief The NcaLoader class allows loading an NCA's ExeFS through the Loader interface (https://switchbrew.org/wiki/NSO)
|
||||
*/
|
||||
class NcaLoader : public Loader {
|
||||
private:
|
||||
vfs::NCA nca; //!< The backing NCA of the loader
|
||||
|
||||
public:
|
||||
NcaLoader(const std::shared_ptr<vfs::Backing> &backing);
|
||||
|
||||
/**
|
||||
* @brief This loads an ExeFS into memory
|
||||
* @param exefs A filesystem object containing the ExeFS filesystem to load into memory
|
||||
* @param process The process to load the ExeFS into
|
||||
*/
|
||||
static void LoadExeFs(const std::shared_ptr<vfs::FileSystem> &exefs, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state);
|
||||
|
||||
void LoadProcessData(const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state);
|
||||
};
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
#include "vfs/os_backing.h"
|
||||
#include "loader/nro.h"
|
||||
#include "loader/nso.h"
|
||||
#include "loader/nca.h"
|
||||
#include "nce/guest.h"
|
||||
#include "os.h"
|
||||
|
||||
@ -17,6 +18,8 @@ namespace skyline::kernel {
|
||||
state.loader = std::make_shared<loader::NroLoader>(romFile);
|
||||
} else if (romType == loader::RomFormat::NSO) {
|
||||
state.loader = std::make_shared<loader::NsoLoader>(romFile);
|
||||
} else if (romType == loader::RomFormat::NCA) {
|
||||
state.loader = std::make_shared<loader::NcaLoader>(romFile);
|
||||
} else {
|
||||
throw exception("Unsupported ROM extension.");
|
||||
}
|
||||
|
53
app/src/main/cpp/skyline/vfs/nca.cpp
Normal file
53
app/src/main/cpp/skyline/vfs/nca.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include "region_backing.h"
|
||||
#include "partition_filesystem.h"
|
||||
#include "nca.h"
|
||||
#include "rom_filesystem.h"
|
||||
#include "directory.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
NCA::NCA(const std::shared_ptr<vfs::Backing> &backing) : backing(backing) {
|
||||
backing->Read(&header);
|
||||
|
||||
if (header.magic != util::MakeMagic<u32>("NCA3"))
|
||||
throw exception("Attempting to load an encrypted or invalid NCA");
|
||||
|
||||
contentType = header.contentType;
|
||||
|
||||
for (size_t i = 0; i < header.sectionHeaders.size(); i++) {
|
||||
auto §ionHeader = header.sectionHeaders.at(i);
|
||||
auto §ionEntry = header.fsEntries.at(i);
|
||||
|
||||
if (sectionHeader.fsType == NcaSectionFsType::PFS0 && sectionHeader.hashType == NcaSectionHashType::HierarchicalSha256)
|
||||
ReadPfs0(sectionHeader, sectionEntry);
|
||||
else if (sectionHeader.fsType == NcaSectionFsType::RomFs && sectionHeader.hashType == NcaSectionHashType::HierarchicalIntegrity)
|
||||
ReadRomFs(sectionHeader, sectionEntry);
|
||||
}
|
||||
}
|
||||
|
||||
void NCA::ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry) {
|
||||
size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.sha256HashInfo.pfs0Offset;
|
||||
size_t size = constant::MediaUnitSize * static_cast<size_t>(entry.endOffset - entry.startOffset);
|
||||
|
||||
auto pfs = std::make_shared<PartitionFileSystem>(std::make_shared<RegionBacking>(backing, offset, size));
|
||||
|
||||
if (contentType == NcaContentType::Program) {
|
||||
// An ExeFS must always contain an NPDM and a main NSO, whereas the logo section will always contain a logo and a startup movie
|
||||
if (pfs->FileExists("main") && pfs->FileExists("main.npdm"))
|
||||
exeFs = std::move(pfs);
|
||||
else if (pfs->FileExists("NintendoLogo.png") && pfs->FileExists("StartupMovie.gif"))
|
||||
logo = std::move(pfs);
|
||||
} else if (contentType == NcaContentType::Meta) {
|
||||
cnmt = std::move(pfs);
|
||||
}
|
||||
}
|
||||
|
||||
void NCA::ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry) {
|
||||
size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.integrityHashInfo.levels.back().offset;
|
||||
size_t size = header.integrityHashInfo.levels.back().size;
|
||||
|
||||
romFs = std::make_shared<RegionBacking>(backing, offset, size);
|
||||
}
|
||||
}
|
213
app/src/main/cpp/skyline/vfs/nca.h
Normal file
213
app/src/main/cpp/skyline/vfs/nca.h
Normal file
@ -0,0 +1,213 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include "filesystem.h"
|
||||
|
||||
namespace skyline {
|
||||
namespace constant {
|
||||
constexpr size_t MediaUnitSize = 0x200; //!< The unit size of entries in an NCA
|
||||
}
|
||||
|
||||
namespace vfs {
|
||||
/**
|
||||
* @brief This enumerates the various content types of an NCA
|
||||
*/
|
||||
enum class NcaContentType : u8 {
|
||||
Program = 0x0, //!< This is a program NCA
|
||||
Meta = 0x1, //!< This is a metadata NCA
|
||||
Control = 0x2, //!< This is a control NCA
|
||||
Manual = 0x3, //!< This is a manual NCA
|
||||
Data = 0x4, //!< This is a data NCA
|
||||
PublicData = 0x5, //!< This is a public data NCA
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The NCA class provides an easy way to access the contents of an NCA file (https://switchbrew.org/wiki/NCA_Format)
|
||||
*/
|
||||
class NCA {
|
||||
private:
|
||||
/**
|
||||
* @brief This enumerates the distribution types of an NCA
|
||||
*/
|
||||
enum class NcaDistributionType : u8 {
|
||||
System = 0x0, //!< This NCA was distributed on the EShop or is part of the system
|
||||
GameCard = 0x1, //!< This NCA was distributed on a GameCard
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the key generation version in NCAs before HOS 3.0.1
|
||||
*/
|
||||
enum class NcaLegacyKeyGenerationType : u8 {
|
||||
Fw100 = 0x0, //!< 1.0.0
|
||||
Fw300 = 0x2, //!< 3.0.0
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the key generation version in NCAs after HOS 3.0.0
|
||||
*/
|
||||
enum class NcaKeyGenerationType : u8 {
|
||||
Fw301 = 0x3, //!< 3.0.1
|
||||
Fw400 = 0x4, //!< 4.0.0
|
||||
Fw500 = 0x5, //!< 5.0.0
|
||||
Fw600 = 0x6, //!< 6.0.0
|
||||
Fw620 = 0x7, //!< 6.2.0
|
||||
Fw700 = 0x8, //!< 7.0.0
|
||||
Fw810 = 0x9, //!< 8.1.0
|
||||
Fw900 = 0xa, //!< 9.0.0
|
||||
Fw910 = 0xb, //!< 9.1.0
|
||||
Invalid = 0xff, //!< An invalid key generation type
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the key area encryption key types
|
||||
*/
|
||||
enum class NcaKeyAreaEncryptionKeyType : u8 {
|
||||
Application = 0x0, //!< This NCA uses the application key encryption area
|
||||
Ocean = 0x1, //!< This NCA uses the ocean key encryption area
|
||||
System = 0x2, //!< This NCA uses the system key encryption area
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This hold the entry of a single filesystem in an NCA
|
||||
*/
|
||||
struct NcaFsEntry {
|
||||
u32 startOffset; //!< The start offset of the filesystem in units of 0x200 bytes
|
||||
u32 endOffset; //!< The start offset of the filesystem in units of 0x200 bytes
|
||||
u64 _pad_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the possible filesystem types a section of an NCA can contain
|
||||
*/
|
||||
enum class NcaSectionFsType : u8 {
|
||||
RomFs = 0x0, //!< This section contains a RomFs filesystem
|
||||
PFS0 = 0x1, //!< This section contains a PFS0 filesystem
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the possible hash header types of an NCA section
|
||||
*/
|
||||
enum class NcaSectionHashType : u8 {
|
||||
HierarchicalSha256 = 0x2, //!< The hash header for this section is that of a PFS0
|
||||
HierarchicalIntegrity = 0x3, //!< The hash header for this section is that of a RomFS
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates the possible encryption types of an NCA section
|
||||
*/
|
||||
enum class NcaSectionEncryptionType : u8 {
|
||||
None = 0x1, //!< This NCA doesn't use any encryption
|
||||
XTS = 0x2, //!< This NCA uses AES-XTS encryption
|
||||
CTR = 0x3, //!< This NCA uses AES-CTR encryption
|
||||
BKTR = 0x4, //!< This NCA uses BKTR together AES-CTR encryption
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This holds the data for a single level of the hierarchical integrity scheme
|
||||
*/
|
||||
struct HierarchicalIntegrityLevel {
|
||||
u64 offset; //!< The offset of the level data
|
||||
u64 size; //!< The size of the level data
|
||||
u32 blockSize; //!< The block size of the level data
|
||||
u32 _pad_;
|
||||
};
|
||||
static_assert(sizeof(HierarchicalIntegrityLevel) == 0x18);
|
||||
|
||||
/**
|
||||
* @brief This holds the hash info header of the hierarchical integrity scheme
|
||||
*/
|
||||
struct HierarchicalIntegrityHashInfo {
|
||||
u32 magic; //!< The hierarchical integrity magic, 'IVFC'
|
||||
u32 magicNumber; //!< The magic number 0x2000
|
||||
u32 masterHashSize; //!< The size of the master hash
|
||||
u32 numLevels; //!< The number of levels
|
||||
std::array<HierarchicalIntegrityLevel, 6> levels; //!< An array of the hierarchical integrity levels
|
||||
u8 _pad0_[0x20];
|
||||
std::array<u8, 0x20> masterHash; //!< The master hash of the hierarchical integrity system
|
||||
u8 _pad1_[0x18];
|
||||
};
|
||||
static_assert(sizeof(HierarchicalIntegrityHashInfo) == 0xf8);
|
||||
|
||||
/**
|
||||
* @brief This holds the hash info header of the SHA256 hashing scheme for PFS0
|
||||
*/
|
||||
struct HierarchicalSha256HashInfo {
|
||||
std::array<u8, 0x20> hashTableHash; //!< A SHA256 hash over the hash table
|
||||
u32 blockSize; //!< The block size of the filesystem
|
||||
u32 _pad_;
|
||||
u64 hashTableOffset; //!< The offset from the end of the section header of the hash table
|
||||
u64 hashTableSize; //!< The size of the hash table
|
||||
u64 pfs0Offset; //!< The offset from the end of the section header of the PFS0
|
||||
u64 pfs0Size; //!< The size of the PFS0
|
||||
u8 _pad1_[0xb0];
|
||||
};
|
||||
static_assert(sizeof(HierarchicalSha256HashInfo) == 0xf8);
|
||||
|
||||
/**
|
||||
* @brief This holds the header of each specific section in an NCA
|
||||
*/
|
||||
struct NcaSectionHeader {
|
||||
u16 version; //!< The version, always 2
|
||||
NcaSectionFsType fsType; //!< The type of the filesystem in the section
|
||||
NcaSectionHashType hashType; //!< The type of hash header that is used for this section
|
||||
NcaSectionEncryptionType encryptionType; //!< The type of encryption that is used for this section
|
||||
u8 _pad0_[0x3];
|
||||
union {
|
||||
HierarchicalIntegrityHashInfo integrityHashInfo; //!< The HashInfo used for RomFS
|
||||
HierarchicalSha256HashInfo sha256HashInfo; //!< The HashInfo used for PFS0
|
||||
};
|
||||
u8 _pad1_[0x40]; // PatchInfo
|
||||
u32 generation; //!< The generation of the NCA section
|
||||
u32 secureValue; //!< The secure value of the section
|
||||
u8 _pad2_[0x30]; //!< SparseInfo
|
||||
u8 _pad3_[0x88];
|
||||
};
|
||||
static_assert(sizeof(NcaSectionHeader) == 0x200);
|
||||
|
||||
/**
|
||||
* @brief This struct holds the header of a Nintendo Content Archive
|
||||
*/
|
||||
struct NcaHeader {
|
||||
std::array<u8, 0x100> fixed_key_sig; //!< An RSA-PSS signature over the header with fixed key
|
||||
std::array<u8, 0x100> npdm_key_sig; //!< An RSA-PSS signature over header with key in NPDM
|
||||
u32 magic; //!< The magic of the NCA: 'NCA3'
|
||||
NcaDistributionType distributionType; //!< Whether this NCA is from a gamecard or the E-Shop
|
||||
NcaContentType contentType; //!< The content type of the NCA
|
||||
NcaLegacyKeyGenerationType legacyKeyGenerationType; //!< The keyblob to use for decryption
|
||||
NcaKeyAreaEncryptionKeyType keyAreaEncryptionKeyType; //!< The index of the key area encryption key that is needed
|
||||
u64 size; //!< The total size of the NCA
|
||||
u64 programId; //!< The program ID of the NCA
|
||||
u32 contentIndex; //!< The index of the content
|
||||
u32 sdkVersion; //!< The version of the SDK the NCA was built with
|
||||
NcaKeyGenerationType keyGenerationType; //!< The keyblob to use for decryption
|
||||
u8 fixedKeyGeneration; //!< The fixed key index
|
||||
u8 _pad0_[0xe];
|
||||
std::array<u8, 0x10> rightsId; //!< The NCA's rights ID
|
||||
std::array<NcaFsEntry, 4> fsEntries; //!< The filesystem entries for this NCA
|
||||
std::array<std::array<u8, 0x20>, 4> sectionHashes; //!< This contains SHA-256 hashes for each filesystem header
|
||||
std::array<std::array<u8, 0x10>, 4> encryptedKeyArea; //!< The encrypted key area for each filesystem
|
||||
u8 _pad1_[0xc0];
|
||||
std::array<NcaSectionHeader, 4> sectionHeaders;
|
||||
} header{};
|
||||
static_assert(sizeof(NcaHeader) == 0xc00);
|
||||
|
||||
std::shared_ptr<Backing> backing; //!< The backing for the NCA
|
||||
|
||||
void ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry);
|
||||
|
||||
void ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry);
|
||||
|
||||
public:
|
||||
std::shared_ptr<FileSystem> exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section
|
||||
std::shared_ptr<FileSystem> logo; //!< The PFS0 filesystem for this NCA's logo section
|
||||
std::shared_ptr<FileSystem> cnmt; //!< The PFS0 filesystem for this NCA's CNMT section
|
||||
std::shared_ptr<Backing> romFs; //!< The backing for this NCA's RomFS section
|
||||
NcaContentType contentType; //!< The content type of the NCA
|
||||
|
||||
NCA(const std::shared_ptr<vfs::Backing> &backing);
|
||||
};
|
||||
}
|
||||
}
|
@ -113,6 +113,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
|
||||
|
||||
var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
|
||||
runOnUiThread {
|
||||
if (!foundRoms)
|
||||
|
@ -25,8 +25,9 @@ import java.util.*
|
||||
enum class RomFormat(val format: Int){
|
||||
NRO(0),
|
||||
NSO(1),
|
||||
XCI(2),
|
||||
NSP(3),
|
||||
NCA(2),
|
||||
XCI(3),
|
||||
NSP(4),
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user