Files
sphaira/sphaira/source/nro.cpp

346 lines
11 KiB
C++

#include "nro.hpp"
#include "defines.hpp"
#include "evman.hpp"
#include "app.hpp"
#include "log.hpp"
#include <switch.h>
#include <vector>
#include <cstring>
#include <string_view>
#include <minIni.h>
namespace sphaira {
namespace {
enum {
Module_Nro = 421,
};
enum NroError {
NroError_BadMagic = MAKERESULT(Module_Nro, 1),
NroError_BadSize = MAKERESULT(Module_Nro, 2),
};
struct NroData {
NroStart start;
NroHeader header;
};
auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entry) -> Result {
entry.path = path;
// todo: special sorting for fw 2.0.0 to make it not look like shit
if (hosversionAtLeast(3,0,0)) {
// it doesn't matter if we fail
entry.timestamp.is_valid = false;
fs.GetFileTimeStampRaw(entry.path, &entry.timestamp);
// if (R_FAILED(fsFsGetFileTimeStampRaw(fs, entry.path, &entry.timestamp))) {
// // log_write("failed to get timestamp for: %s\n", path);
// }
}
FsFile f;
R_TRY(fs.OpenFile(entry.path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileGetSize(&f, &entry.size));
NroData data;
u64 bytes_read;
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
NroAssetHeader asset;
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
// some .nro (vgedit) have bad nacp, fake the nacp
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(entry.nacp)) {
std::memset(&entry.nacp, 0, sizeof(entry.nacp));
// get the name without the .nro
const auto file_name = std::strrchr(path, '/') + 1;
const auto file_name_len = std::strlen(file_name);
for (auto& lang : entry.nacp.lang) {
std::strncpy(lang.name, file_name, file_name_len - 4);
std::strcpy(lang.author, "Unknown");
}
std::strcpy(entry.nacp.display_version, "Unknown");
entry.is_nacp_valid = false;
} else {
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &entry.nacp, sizeof(entry.nacp), FsReadOption_None, &bytes_read));
entry.is_nacp_valid = true;
}
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
R_SUCCEED();
}
// this function is recursive by 1 level deep
// if the nro is in switch/folder/folder2/app.nro it will NOT be found
// switch/folder/app.nro for example will work fine.
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
// we don't need to scan for folders if we are not root
u32 dir_open_type = FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize;
if (root) {
dir_open_type |= FsDirOpenMode_ReadDirs;
}
FsDir d;
R_TRY(fs.OpenDirectory(path, dir_open_type, &d));
ON_SCOPE_EXIT(fs.DirClose(&d));
s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count));
// return early if empty
R_UNLESS(count > 0, 0x0);
// we won't run out of memory here
std::vector<FsDirectoryEntry> entries(count);
R_TRY(fs.DirRead(&d, &count, entries.size(), entries.data()));
// size may of changed
entries.resize(count);
for (const auto& e : entries) {
// skip hidden files / folders
if ('.' == e.name[0]) {
continue;
}
// skip self
if (hide_sphaira && !strncmp(e.name, "sphaira", strlen("sphaira"))) {
continue;
}
if (e.type == FsDirEntryType_Dir) {
// assert(!root && "dir should only be scanned on non-root!");
fs::FsPath fullpath;
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path.s, e.name, e.name);
// fast path for detecting an nro in a folder
NroEntry entry;
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
// log_write("NRO: fast path for: %s\n", fullpath);
nros.emplace_back(entry);
} else {
// slow path...
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
}
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
fs::FsPath fullpath;
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
NroEntry entry;
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
nros.emplace_back(entry);
if (!root && !scan_all_dir) {
// log_write("NRO: slow path for: %s\n", fullpath);
R_SUCCEED();
}
} else {
log_write("error when trying to parse %s\n", fullpath.s);
}
}
}
R_SUCCEED();
}
auto nro_get_icon_internal(FsFile* f, u64 size, u64 offset) -> std::vector<u8> {
std::vector<u8> icon;
u64 bytes_read{};
icon.resize(size);
R_TRY_RESULT(fsFileRead(f, offset, icon.data(), icon.size(), FsReadOption_None, &bytes_read), {});
R_UNLESS(bytes_read == icon.size(), {});
return icon;
}
auto launch_internal(const std::string& path, const std::string& argv) -> Result {
R_TRY(envSetNextLoad(path.c_str(), argv.c_str()));
log_write("set launch with path: %s argv: %s\n", path.c_str(), argv.c_str());
evman::push(evman::LaunchNroEventData{path, argv});
R_SUCCEED();
}
} // namespace
/*
NRO INFO PAGE:
- icon
- name
- author
- path
- filesize
- launch count
- timestamp created
- timestamp modified
*/
auto nro_verify(std::span<const u8> data) -> Result {
NroData nro;
R_UNLESS(data.size() >= sizeof(nro), NroError_BadSize);
memcpy(&nro, data.data(), sizeof(nro));
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_SUCCEED();
}
auto nro_parse(const fs::FsPath& path, NroEntry& entry) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return nro_parse_internal(fs, path, entry);
}
auto nro_scan(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir) -> Result {
return nro_scan_internal(path, nros, hide_sphaira, nested, scan_all_dir, true);
}
auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
return nro_get_icon_internal(&f, size, offset);
}
auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY_RESULT(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read), {});
R_UNLESS(data.header.magic == NROHEADER_MAGIC, {});
R_TRY_RESULT(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {});
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, {});
return nro_get_icon_internal(&f, asset.icon.size, data.header.size + asset.icon.offset);
}
auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
R_SUCCEED();
}
auto nro_launch(std::string path, std::string args) -> Result {
if (path.empty()) {
return 1;
}
// keeps compat with hbloader
// https://github.com/ITotalJustice/Gamecard-Installer-NX/blob/master/source/main.c#L73
// https://github.com/ITotalJustice/Gamecard-Installer-NX/blob/d549c5f916dea814fa0a7e5dc8c903fa3044ba15/source/main.c#L29
if (!path.starts_with("sdmc:")) {
path = "sdmc:" + path;
}
if (args.empty()) {
args = nro_add_arg(path);
} else {
args = nro_add_arg(path) + ' ' + args;
}
return launch_internal(path, args);
}
auto nro_add_arg(std::string arg) -> std::string {
if (arg.contains(' ')) {
return '\"' + arg + '\"';
}
return arg;
}
auto nro_add_arg_file(std::string arg) -> std::string {
if (!arg.starts_with("sdmc:")) {
arg = "sdmc:" + arg;
}
if (arg.contains(' ')) {
return '\"' + arg + '\"';
}
return arg;
}
auto nro_normalise_path(const std::string& p) -> std::string {
if (p.starts_with("sdmc:")) {
return p.substr(5);
}
return p;
}
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry> {
const auto it = std::find_if(array.cbegin(), array.cend(), [name, author, path](auto& e){
if (!name.empty() && !author.empty() && !path.empty()) {
return e.GetName() == name && e.GetAuthor() == author && e.path == path;
} else if (!name.empty()) {
return e.GetName() == name;
} else if (!author.empty()) {
return e.GetAuthor() == author;
} else if (!path.empty()) {
return e.path == path;
}
return false;
});
if (it == array.cend()) {
return std::nullopt;
}
return *it;
}
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry> {
return nro_find(array, name, {}, {});
}
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry> {
return nro_find(array, {}, author, {});
}
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry> {
return nro_find(array, {}, {}, path);
}
} // namespace sphaira