mirror of
https://github.com/Atmosphere-NX/Atmosphere.git
synced 2024-11-28 09:30:58 +01:00
ams_mitm: implement layeredfs
This commit is contained in:
parent
ad64cb5212
commit
733f2b3cdd
@ -19,6 +19,8 @@
|
||||
|
||||
namespace ams::mitm::fs {
|
||||
|
||||
using namespace ams::fs;
|
||||
|
||||
namespace {
|
||||
|
||||
/* Globals. */
|
||||
@ -46,6 +48,18 @@ namespace ams::mitm::fs {
|
||||
}
|
||||
}
|
||||
|
||||
void FormatAtmosphereSdPath(char *dst_path, size_t dst_path_size, ncm::ProgramId program_id, const char *subdir, const char *src_path) {
|
||||
if (src_path[0] == '/') {
|
||||
std::snprintf(dst_path, dst_path_size, "/atmosphere/contents/%016lx/%s%s", static_cast<u64>(program_id), subdir, src_path);
|
||||
} else {
|
||||
std::snprintf(dst_path, dst_path_size, "/atmosphere/contents/%016lx/%s/%s", static_cast<u64>(program_id), subdir, src_path);
|
||||
}
|
||||
}
|
||||
|
||||
void FormatAtmosphereRomfsPath(char *dst_path, size_t dst_path_size, ncm::ProgramId program_id, const char *src_path) {
|
||||
return FormatAtmosphereSdPath(dst_path, dst_path_size, program_id, "romfs", src_path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenGlobalSdCardFileSystem() {
|
||||
@ -69,4 +83,95 @@ namespace ams::mitm::fs {
|
||||
return OpenSdFile(out, fixed_path, mode);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereSdRomfsFile(FsFile *out, ncm::ProgramId program_id, const char *path, u32 mode) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereRomfsPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
return OpenSdFile(out, fixed_path, mode);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereRomfsFile(FsFile *out, ncm::ProgramId program_id, const char *path, u32 mode, FsFileSystem *fs) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereRomfsPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
return fsFsOpenFile(fs, fixed_path, mode, out);
|
||||
}
|
||||
|
||||
Result OpenSdDirectory(FsDir *out, const char *path, u32 mode) {
|
||||
R_TRY(EnsureSdInitialized());
|
||||
return fsFsOpenDirectory(&g_sd_filesystem, path, mode, out);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereSdDirectory(FsDir *out, const char *path, u32 mode) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereSdPath(fixed_path, sizeof(fixed_path), path);
|
||||
return OpenSdDirectory(out, fixed_path, mode);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereSdDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereSdPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
return OpenSdDirectory(out, fixed_path, mode);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereSdRomfsDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereRomfsPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
return OpenSdDirectory(out, fixed_path, mode);
|
||||
}
|
||||
|
||||
Result OpenAtmosphereRomfsDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode, FsFileSystem *fs) {
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereRomfsPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
return fsFsOpenDirectory(fs, fixed_path, mode, out);
|
||||
}
|
||||
|
||||
|
||||
bool HasSdRomfsContent(ncm::ProgramId program_id) {
|
||||
/* Check if romfs.bin is present. */
|
||||
{
|
||||
FsFile romfs_file;
|
||||
if (R_SUCCEEDED(OpenAtmosphereSdFile(&romfs_file, program_id, "romfs.bin", OpenMode_Read))) {
|
||||
fsFileClose(&romfs_file);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for romfs folder with content. */
|
||||
FsDir romfs_dir;
|
||||
if (R_FAILED(OpenAtmosphereSdRomfsDirectory(&romfs_dir, program_id, "", OpenDirectoryMode_All))) {
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT { fsDirClose(&romfs_dir); };
|
||||
|
||||
/* Verify the folder has at least one entry. */
|
||||
s64 num_entries = 0;
|
||||
return R_SUCCEEDED(fsDirGetEntryCount(&romfs_dir, &num_entries)) && num_entries > 0;
|
||||
}
|
||||
|
||||
Result SaveAtmosphereSdFile(FsFile *out, ncm::ProgramId program_id, const char *path, void *data, size_t size) {
|
||||
R_TRY(EnsureSdInitialized());
|
||||
|
||||
char fixed_path[FS_MAX_PATH];
|
||||
FormatAtmosphereSdPath(fixed_path, sizeof(fixed_path), program_id, path);
|
||||
|
||||
/* Unconditionally create. */
|
||||
/* Don't check error, as a failure here should be okay. */
|
||||
FsFile f;
|
||||
fsFsCreateFile(&g_sd_filesystem, fixed_path, size, 0);
|
||||
|
||||
/* Try to open. */
|
||||
R_TRY(fsFsOpenFile(&g_sd_filesystem, fixed_path, OpenMode_ReadWrite, &f));
|
||||
auto file_guard = SCOPE_GUARD { fsFileClose(&f); };
|
||||
|
||||
/* Try to set the size. */
|
||||
R_TRY(fsFileSetSize(&f, static_cast<s64>(size)));
|
||||
|
||||
/* Try to write data. */
|
||||
R_TRY(fsFileWrite(&f, 0, data, size, FsWriteOption_Flush));
|
||||
|
||||
/* Set output. */
|
||||
file_guard.Cancel();
|
||||
*out = f;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,5 +25,17 @@ namespace ams::mitm::fs {
|
||||
Result OpenSdFile(FsFile *out, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdFile(FsFile *out, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdFile(FsFile *out, ncm::ProgramId program_id, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdRomfsFile(FsFile *out, ncm::ProgramId program_id, const char *path, u32 mode);
|
||||
Result OpenAtmosphereRomfsFile(FsFile *out, ncm::ProgramId program_id, const char *path, u32 mode, FsFileSystem *fs);
|
||||
|
||||
Result OpenSdDirectory(FsDir *out, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdDirectory(FsDir *out, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode);
|
||||
Result OpenAtmosphereSdRomfsDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode);
|
||||
Result OpenAtmosphereRomfsDirectory(FsDir *out, ncm::ProgramId program_id, const char *path, u32 mode, FsFileSystem *fs);
|
||||
|
||||
bool HasSdRomfsContent(ncm::ProgramId program_id);
|
||||
|
||||
Result SaveAtmosphereSdFile(FsFile *out, ncm::ProgramId program_id, const char *path, void *data, size_t size);
|
||||
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ namespace ams::mitm::bpc {
|
||||
|
||||
/* Open payload file. */
|
||||
FsFile payload_file;
|
||||
R_TRY(fs::OpenAtmosphereSdFile(&payload_file, "/reboot_payload.bin", FsOpenMode_Read));
|
||||
R_TRY(fs::OpenAtmosphereSdFile(&payload_file, "/reboot_payload.bin", ams::fs::OpenMode_Read));
|
||||
ON_SCOPE_EXIT { fsFileClose(&payload_file); };
|
||||
|
||||
/* Read payload file. Discard result. */
|
||||
|
@ -13,9 +13,11 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "../amsmitm_fs_utils.hpp"
|
||||
#include "fs_shim.h"
|
||||
#include "fs_mitm_service.hpp"
|
||||
#include "fsmitm_boot0storage.hpp"
|
||||
#include "fsmitm_layered_romfs_storage.hpp"
|
||||
|
||||
namespace ams::mitm::fs {
|
||||
|
||||
@ -23,6 +25,35 @@ namespace ams::mitm::fs {
|
||||
|
||||
namespace {
|
||||
|
||||
os::Mutex g_storage_cache_lock;
|
||||
std::unordered_map<u64, std::weak_ptr<IStorageInterface>> g_storage_cache;
|
||||
|
||||
std::shared_ptr<IStorageInterface> GetStorageCacheEntry(ncm::ProgramId program_id) {
|
||||
std::scoped_lock lk(g_storage_cache_lock);
|
||||
|
||||
auto it = g_storage_cache.find(static_cast<u64>(program_id));
|
||||
if (it == g_storage_cache.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return it->second.lock();
|
||||
}
|
||||
|
||||
void SetStorageCacheEntry(ncm::ProgramId program_id, std::shared_ptr<IStorageInterface> *new_intf) {
|
||||
std::scoped_lock lk(g_storage_cache_lock);
|
||||
|
||||
auto it = g_storage_cache.find(static_cast<u64>(program_id));
|
||||
if (it != g_storage_cache.end()) {
|
||||
auto cur_intf = it->second.lock();
|
||||
if (cur_intf != nullptr) {
|
||||
*new_intf = cur_intf;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_storage_cache[static_cast<u64>(program_id)] = *new_intf;
|
||||
}
|
||||
|
||||
bool GetSettingsItemBooleanValue(const char *name, const char *key) {
|
||||
u8 tmp = 0;
|
||||
AMS_ASSERT(settings::fwdbg::GetSettingsItemValue(&tmp, sizeof(tmp), name, key) == sizeof(tmp));
|
||||
@ -77,4 +108,88 @@ namespace ams::mitm::fs {
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result FsMitmService::OpenDataStorageByCurrentProcess(sf::Out<std::shared_ptr<IStorageInterface>> out) {
|
||||
/* Only mitm if we should override contents for the current process. */
|
||||
R_UNLESS(this->client_info.override_status.IsProgramSpecific(), sm::mitm::ResultShouldForwardToSession());
|
||||
|
||||
/* Only mitm if there is actually an override romfs. */
|
||||
R_UNLESS(mitm::fs::HasSdRomfsContent(this->client_info.program_id), sm::mitm::ResultShouldForwardToSession());
|
||||
|
||||
/* Try to open the process romfs. */
|
||||
FsStorage data_storage;
|
||||
R_TRY(fsOpenDataStorageByCurrentProcessFwd(this->forward_service.get(), &data_storage));
|
||||
const sf::cmif::DomainObjectId target_object_id{data_storage.s.object_id};
|
||||
|
||||
/* Try to get a storage from the cache. */
|
||||
{
|
||||
std::shared_ptr<IStorageInterface> cached_storage = GetStorageCacheEntry(this->client_info.program_id);
|
||||
if (cached_storage != nullptr) {
|
||||
out.SetValue(std::move(cached_storage), target_object_id);
|
||||
return ResultSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/* Make a new layered romfs, and cache to storage. */
|
||||
{
|
||||
std::shared_ptr<IStorageInterface> new_storage_intf = nullptr;
|
||||
|
||||
/* Create the layered storage. */
|
||||
FsFile data_file;
|
||||
if (R_SUCCEEDED(OpenAtmosphereSdFile(&data_file, this->client_info.program_id, "romfs.bin", OpenMode_Read))) {
|
||||
auto *layered_storage = new LayeredRomfsStorage(std::make_unique<ReadOnlyStorageAdapter>(new RemoteStorage(data_storage)), std::make_unique<ReadOnlyStorageAdapter>(new FileStorage(new RemoteFile(data_file))), this->client_info.program_id);
|
||||
new_storage_intf = std::make_shared<IStorageInterface>(layered_storage);
|
||||
} else {
|
||||
auto *layered_storage = new LayeredRomfsStorage(std::make_unique<ReadOnlyStorageAdapter>(new RemoteStorage(data_storage)), nullptr, this->client_info.program_id);
|
||||
new_storage_intf = std::make_shared<IStorageInterface>(layered_storage);
|
||||
}
|
||||
|
||||
SetStorageCacheEntry(this->client_info.program_id, &new_storage_intf);
|
||||
out.SetValue(std::move(new_storage_intf), target_object_id);
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result FsMitmService::OpenDataStorageByDataId(sf::Out<std::shared_ptr<IStorageInterface>> out, ncm::ProgramId /* TODO: ncm::DataId */ data_id, u8 storage_id) {
|
||||
/* Only mitm if we should override contents for the current process. */
|
||||
R_UNLESS(this->client_info.override_status.IsProgramSpecific(), sm::mitm::ResultShouldForwardToSession());
|
||||
|
||||
/* Only mitm if there is actually an override romfs. */
|
||||
R_UNLESS(mitm::fs::HasSdRomfsContent(data_id), sm::mitm::ResultShouldForwardToSession());
|
||||
|
||||
/* Try to open the process romfs. */
|
||||
FsStorage data_storage;
|
||||
R_TRY(fsOpenDataStorageByDataIdFwd(this->forward_service.get(), &data_storage, static_cast<u64>(data_id), static_cast<NcmStorageId>(storage_id)));
|
||||
const sf::cmif::DomainObjectId target_object_id{data_storage.s.object_id};
|
||||
|
||||
/* Try to get a storage from the cache. */
|
||||
{
|
||||
std::shared_ptr<IStorageInterface> cached_storage = GetStorageCacheEntry(data_id);
|
||||
if (cached_storage != nullptr) {
|
||||
out.SetValue(std::move(cached_storage), target_object_id);
|
||||
return ResultSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/* Make a new layered romfs, and cache to storage. */
|
||||
{
|
||||
std::shared_ptr<IStorageInterface> new_storage_intf = nullptr;
|
||||
|
||||
/* Create the layered storage. */
|
||||
FsFile data_file;
|
||||
if (R_SUCCEEDED(OpenAtmosphereSdFile(&data_file, data_id, "romfs.bin", OpenMode_Read))) {
|
||||
auto *layered_storage = new LayeredRomfsStorage(std::make_unique<ReadOnlyStorageAdapter>(new RemoteStorage(data_storage)), std::make_unique<ReadOnlyStorageAdapter>(new FileStorage(new RemoteFile(data_file))), data_id);
|
||||
new_storage_intf = std::make_shared<IStorageInterface>(layered_storage);
|
||||
} else {
|
||||
auto *layered_storage = new LayeredRomfsStorage(std::make_unique<ReadOnlyStorageAdapter>(new RemoteStorage(data_storage)), nullptr, data_id);
|
||||
new_storage_intf = std::make_shared<IStorageInterface>(layered_storage);
|
||||
}
|
||||
|
||||
SetStorageCacheEntry(data_id, &new_storage_intf);
|
||||
out.SetValue(std::move(new_storage_intf), target_object_id);
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -62,8 +62,8 @@ namespace ams::mitm::fs {
|
||||
/* Result OpenSdCardFileSystem(Out<std::shared_ptr<IFileSystemInterface>> out); */
|
||||
/* Result OpenSaveDataFileSystem(Out<std::shared_ptr<IFileSystemInterface>> out, u8 space_id, FsSave save_struct); */
|
||||
Result OpenBisStorage(sf::Out<std::shared_ptr<IStorageInterface>> out, u32 bis_partition_id);
|
||||
/* Result OpenDataStorageByCurrentProcess(Out<std::shared_ptr<IStorageInterface>> out); */
|
||||
/* Result OpenDataStorageByDataId(Out<std::shared_ptr<IStorageInterface>> out, u64 data_id, u8 storage_id); */
|
||||
Result OpenDataStorageByCurrentProcess(sf::Out<std::shared_ptr<IStorageInterface>> out);
|
||||
Result OpenDataStorageByDataId(sf::Out<std::shared_ptr<IStorageInterface>> out, ncm::ProgramId /* TODO: ncm::DataId */ data_id, u8 storage_id);
|
||||
public:
|
||||
DEFINE_SERVICE_DISPATCH_TABLE {
|
||||
/* MAKE_SERVICE_COMMAND_META(OpenFileSystemWithPatch, hos::Version_200), */
|
||||
@ -71,8 +71,8 @@ namespace ams::mitm::fs {
|
||||
/* MAKE_SERVICE_COMMAND_META(OpenSdCardFileSystem), */
|
||||
/* MAKE_SERVICE_COMMAND_META(OpenSaveDataFileSystem), */
|
||||
MAKE_SERVICE_COMMAND_META(OpenBisStorage),
|
||||
/* MAKE_SERVICE_COMMAND_META(OpenDataStorageByCurrentProcess), */
|
||||
/* MAKE_SERVICE_COMMAND_META(OpenDataStorageByDataId), */
|
||||
MAKE_SERVICE_COMMAND_META(OpenDataStorageByCurrentProcess),
|
||||
MAKE_SERVICE_COMMAND_META(OpenDataStorageByDataId),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -23,3 +23,22 @@ Result fsOpenBisStorageFwd(Service* s, FsStorage* out, FsBisPartitionId partitio
|
||||
.out_objects = &out->s,
|
||||
);
|
||||
}
|
||||
|
||||
Result fsOpenDataStorageByCurrentProcessFwd(Service* s, FsStorage* out) {
|
||||
return serviceDispatch(s, 200,
|
||||
.out_num_objects = 1,
|
||||
.out_objects = &out->s,
|
||||
);
|
||||
}
|
||||
|
||||
Result fsOpenDataStorageByDataIdFwd(Service* s, FsStorage* out, u64 data_id, NcmStorageId storage_id) {
|
||||
const struct {
|
||||
u8 storage_id;
|
||||
u64 data_id;
|
||||
} in = { storage_id, data_id };
|
||||
|
||||
return serviceDispatchIn(s, 202, in,
|
||||
.out_num_objects = 1,
|
||||
.out_objects = &out->s,
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,9 @@ extern "C" {
|
||||
|
||||
/* Missing fsp-srv commands. */
|
||||
Result fsOpenBisStorageFwd(Service* s, FsStorage* out, FsBisPartitionId partition_id);
|
||||
Result fsOpenDataStorageByCurrentProcessFwd(Service* s, FsStorage* out);
|
||||
Result fsOpenDataStorageByDataIdFwd(Service* s, FsStorage* out, u64 data_id, NcmStorageId storage_id);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
#include <stratosphere.hpp>
|
||||
#include "../amsmitm_initialization.hpp"
|
||||
#include "../amsmitm_fs_utils.hpp"
|
||||
#include "fsmitm_layered_romfs_storage.hpp"
|
||||
|
||||
namespace ams::mitm::fs {
|
||||
|
||||
using namespace ams::fs;
|
||||
|
||||
LayeredRomfsStorage::LayeredRomfsStorage(std::unique_ptr<IStorage> s_r, std::unique_ptr<IStorage> f_r, ncm::ProgramId pr_id) : storage_romfs(std::move(s_r)), file_romfs(std::move(f_r)), program_id(std::move(pr_id)) {
|
||||
/* Build new virtual romfs. */
|
||||
romfs::Builder builder(this->program_id);
|
||||
|
||||
if (mitm::IsInitialized()) {
|
||||
builder.AddSdFiles();
|
||||
}
|
||||
if (this->file_romfs) {
|
||||
builder.AddStorageFiles(this->file_romfs.get(), romfs::DataSourceType::File);
|
||||
}
|
||||
if (this->storage_romfs) {
|
||||
builder.AddStorageFiles(this->storage_romfs.get(), romfs::DataSourceType::Storage);
|
||||
}
|
||||
|
||||
builder.Build(&this->source_infos);
|
||||
}
|
||||
|
||||
LayeredRomfsStorage::~LayeredRomfsStorage() {
|
||||
for (size_t i = 0; i < this->source_infos.size(); i++) {
|
||||
this->source_infos[i].Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
Result LayeredRomfsStorage::Read(s64 offset, void *buffer, size_t size) {
|
||||
/* Check if we can succeed immediately. */
|
||||
R_UNLESS(size >= 0, fs::ResultInvalidSize());
|
||||
R_UNLESS(size > 0, ResultSuccess());
|
||||
|
||||
|
||||
/* Validate offset/size. */
|
||||
const s64 virt_size = this->GetSize();
|
||||
R_UNLESS(offset >= 0, fs::ResultInvalidOffset());
|
||||
R_UNLESS(offset < virt_size, fs::ResultInvalidOffset());
|
||||
if (size_t(virt_size - offset) < size) {
|
||||
size = size_t(virt_size - offset);
|
||||
}
|
||||
|
||||
/* Find first source info via binary search. */
|
||||
auto it = std::lower_bound(this->source_infos.begin(), this->source_infos.end(), offset);
|
||||
u8 *cur_dst = static_cast<u8 *>(buffer);
|
||||
|
||||
/* Our operator < compares against start of info instead of end, so we need to subtract one from lower bound. */
|
||||
it--;
|
||||
|
||||
size_t read_so_far = 0;
|
||||
while (read_so_far < size) {
|
||||
const auto &cur_source = *it;
|
||||
AMS_ASSERT(offset >= cur_source.virtual_offset);
|
||||
|
||||
if (offset <= cur_source.virtual_offset + cur_source.size) {
|
||||
const s64 offset_within_source = offset - cur_source.virtual_offset;
|
||||
const size_t cur_read_size = std::min(size - read_so_far, size_t(cur_source.size - offset_within_source));
|
||||
switch (cur_source.source_type) {
|
||||
case romfs::DataSourceType::Storage:
|
||||
R_ASSERT(this->storage_romfs->Read(cur_source.storage_source_info.offset + offset_within_source, cur_dst, cur_read_size));
|
||||
break;
|
||||
case romfs::DataSourceType::File:
|
||||
R_ASSERT(this->file_romfs->Read(cur_source.file_source_info.offset + offset_within_source, cur_dst, cur_read_size));
|
||||
break;
|
||||
case romfs::DataSourceType::LooseSdFile:
|
||||
{
|
||||
FsFile file;
|
||||
R_ASSERT(mitm::fs::OpenAtmosphereSdRomfsFile(&file, this->program_id, cur_source.loose_source_info.path, OpenMode_Read));
|
||||
ON_SCOPE_EXIT { fsFileClose(&file); };
|
||||
|
||||
u64 out_read = 0;
|
||||
R_ASSERT(fsFileRead(&file, offset_within_source, cur_dst, cur_read_size, FsReadOption_None, &out_read));
|
||||
AMS_ASSERT(out_read == cur_read_size);
|
||||
}
|
||||
break;
|
||||
case romfs::DataSourceType::Memory:
|
||||
std::memcpy(cur_dst, cur_source.memory_source_info.data + offset_within_source, cur_read_size);
|
||||
break;
|
||||
case romfs::DataSourceType::Metadata:
|
||||
{
|
||||
size_t out_read = 0;
|
||||
R_ASSERT(cur_source.metadata_source_info.file->Read(&out_read, offset_within_source, cur_dst, cur_read_size));
|
||||
AMS_ASSERT(out_read == cur_read_size);
|
||||
}
|
||||
break;
|
||||
AMS_UNREACHABLE_DEFAULT_CASE();
|
||||
}
|
||||
read_so_far += cur_read_size;
|
||||
cur_dst += cur_read_size;
|
||||
offset += cur_read_size;
|
||||
} else {
|
||||
/* Explicitly handle padding. */
|
||||
const auto &next_source = *(++it);
|
||||
const size_t padding_size = size_t(next_source.virtual_offset - offset);
|
||||
|
||||
std::memset(cur_dst, 0, padding_size);
|
||||
read_so_far += padding_size;
|
||||
cur_dst += padding_size;
|
||||
offset += padding_size;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result LayeredRomfsStorage::GetSize(s64 *out_size) {
|
||||
*out_size = this->GetSize();
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result LayeredRomfsStorage::Flush() {
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result LayeredRomfsStorage::OperateRange(void *dst, size_t dst_size, OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) {
|
||||
switch (op_id) {
|
||||
case OperationId::InvalidateCache:
|
||||
case OperationId::QueryRange:
|
||||
if (size == 0) {
|
||||
if (op_id == OperationId::QueryRange) {
|
||||
R_UNLESS(dst != nullptr, fs::ResultNullptrArgument());
|
||||
R_UNLESS(dst_size == sizeof(QueryRangeInfo), fs::ResultInvalidSize());
|
||||
reinterpret_cast<QueryRangeInfo *>(dst)->Clear();
|
||||
}
|
||||
return ResultSuccess();
|
||||
}
|
||||
/* TODO: How to deal with this? */
|
||||
return fs::ResultUnsupportedOperation();
|
||||
default:
|
||||
return fs::ResultUnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stratosphere.hpp>
|
||||
#include "fsmitm_romfs.hpp"
|
||||
|
||||
namespace ams::mitm::fs {
|
||||
|
||||
class LayeredRomfsStorage : public ams::fs::IStorage {
|
||||
private:
|
||||
std::vector<romfs::SourceInfo> source_infos;
|
||||
std::unique_ptr<ams::fs::IStorage> storage_romfs;
|
||||
std::unique_ptr<ams::fs::IStorage> file_romfs;
|
||||
ncm::ProgramId program_id;
|
||||
protected:
|
||||
inline s64 GetSize() const {
|
||||
const auto &back = this->source_infos.back();
|
||||
return back.virtual_offset + back.size;
|
||||
}
|
||||
public:
|
||||
LayeredRomfsStorage(std::unique_ptr<ams::fs::IStorage> s_r, std::unique_ptr<ams::fs::IStorage> f_r, ncm::ProgramId pr_id);
|
||||
virtual ~LayeredRomfsStorage();
|
||||
|
||||
virtual Result Read(s64 offset, void *buffer, size_t size) override;
|
||||
virtual Result GetSize(s64 *out_size) override;
|
||||
virtual Result Flush() override;
|
||||
virtual Result OperateRange(void *dst, size_t dst_size, ams::fs::OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) override;
|
||||
};
|
||||
|
||||
}
|
453
stratosphere/ams_mitm/source/fs_mitm/fsmitm_romfs.cpp
Normal file
453
stratosphere/ams_mitm/source/fs_mitm/fsmitm_romfs.cpp
Normal file
@ -0,0 +1,453 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
#include <stratosphere.hpp>
|
||||
#include "../amsmitm_fs_utils.hpp"
|
||||
#include "fsmitm_romfs.hpp"
|
||||
|
||||
namespace ams::mitm::fs {
|
||||
|
||||
using namespace ams::fs;
|
||||
|
||||
namespace romfs {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 EmptyEntry = 0xFFFFFFFF;
|
||||
constexpr size_t FilePartitionOffset = 0x200;
|
||||
|
||||
struct Header {
|
||||
s64 header_size;
|
||||
s64 dir_hash_table_ofs;
|
||||
s64 dir_hash_table_size;
|
||||
s64 dir_table_ofs;
|
||||
s64 dir_table_size;
|
||||
s64 file_hash_table_ofs;
|
||||
s64 file_hash_table_size;
|
||||
s64 file_table_ofs;
|
||||
s64 file_table_size;
|
||||
s64 file_partition_ofs;
|
||||
};
|
||||
static_assert(std::is_pod<Header>::value && sizeof(Header) == 0x50);
|
||||
|
||||
struct DirectoryEntry {
|
||||
u32 parent;
|
||||
u32 sibling;
|
||||
u32 child;
|
||||
u32 file;
|
||||
u32 hash;
|
||||
u32 name_size;
|
||||
char name[];
|
||||
};
|
||||
static_assert(std::is_pod<DirectoryEntry>::value && sizeof(DirectoryEntry) == 0x18);
|
||||
|
||||
struct FileEntry {
|
||||
u32 parent;
|
||||
u32 sibling;
|
||||
s64 offset;
|
||||
s64 size;
|
||||
u32 hash;
|
||||
u32 name_size;
|
||||
char name[];
|
||||
};
|
||||
static_assert(std::is_pod<FileEntry>::value && sizeof(FileEntry) == 0x20);
|
||||
|
||||
constexpr inline DirectoryEntry *GetDirectoryEntry(void *dir_table, u32 offset) {
|
||||
return reinterpret_cast<DirectoryEntry *>(reinterpret_cast<uintptr_t>(dir_table) + offset);
|
||||
}
|
||||
|
||||
constexpr inline FileEntry *GetFileEntry(void *file_table, u32 offset) {
|
||||
return reinterpret_cast<FileEntry *>(reinterpret_cast<uintptr_t>(file_table) + offset);
|
||||
}
|
||||
|
||||
constexpr inline const DirectoryEntry *GetDirectoryEntry(const void *dir_table, u32 offset) {
|
||||
return reinterpret_cast<const DirectoryEntry *>(reinterpret_cast<uintptr_t>(dir_table) + offset);
|
||||
}
|
||||
|
||||
constexpr inline const FileEntry *GetFileEntry(const void *file_table, u32 offset) {
|
||||
return reinterpret_cast<const FileEntry *>(reinterpret_cast<uintptr_t>(file_table) + offset);
|
||||
}
|
||||
|
||||
constexpr inline u32 CalculatePathHash(u32 parent, const char *_path, u32 start, size_t path_len) {
|
||||
const unsigned char *path = reinterpret_cast<const unsigned char *>(_path);
|
||||
u32 hash = parent ^ 123456789;
|
||||
for (size_t i = 0; i < path_len; i++) {
|
||||
hash = (hash >> 5) | (hash << 27);
|
||||
hash ^= path[start + i];
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
constexpr inline size_t GetHashTableSize(size_t num_entries) {
|
||||
if (num_entries < 3) {
|
||||
return 3;
|
||||
} else if (num_entries < 19) {
|
||||
return num_entries | 1;
|
||||
} else {
|
||||
size_t count = num_entries;
|
||||
while ((count % 2 == 0) ||
|
||||
(count % 3 == 0) ||
|
||||
(count % 5 == 0) ||
|
||||
(count % 7 == 0) ||
|
||||
(count % 11 == 0) ||
|
||||
(count % 13 == 0) ||
|
||||
(count % 17 == 0))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Builder::Builder(ncm::ProgramId pr_id) : program_id(pr_id), num_dirs(0), num_files(0), dir_table_size(0), file_table_size(0), dir_hash_table_size(0), file_hash_table_size(0), file_partition_size(0) {
|
||||
auto res = this->directories.emplace("", std::make_unique<BuildDirectoryContext>(BuildDirectoryContext::RootTag{}));
|
||||
AMS_ASSERT(res.second);
|
||||
this->root = res.first->second.get();
|
||||
this->num_dirs = 1;
|
||||
this->dir_table_size = 0x18;
|
||||
}
|
||||
|
||||
void Builder::AddDirectory(BuildDirectoryContext **out, BuildDirectoryContext *parent_ctx, std::unique_ptr<BuildDirectoryContext> child_ctx) {
|
||||
/* Check if the directory already exists. */
|
||||
auto existing = this->directories.find(child_ctx->path.get());
|
||||
if (existing != this->directories.end()) {
|
||||
*out = existing->second.get();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Add a new directory. */
|
||||
this->num_dirs++;
|
||||
this->dir_table_size += sizeof(DirectoryEntry) + util::AlignUp(child_ctx->path_len - child_ctx->cur_path_ofs, 4);
|
||||
child_ctx->parent = parent_ctx;
|
||||
|
||||
*out = child_ctx.get();
|
||||
this->directories.emplace(child_ctx->path.get(), std::move(child_ctx));
|
||||
}
|
||||
|
||||
void Builder::AddFile(BuildDirectoryContext *parent_ctx, std::unique_ptr<BuildFileContext> file_ctx) {
|
||||
/* Check if the file already exists. */
|
||||
if (this->files.find(file_ctx->path.get()) != this->files.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Add a new file. */
|
||||
this->num_files++;
|
||||
this->file_table_size += sizeof(FileEntry) + util::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4);
|
||||
file_ctx->parent = parent_ctx;
|
||||
this->files.emplace(file_ctx->path.get(), std::move(file_ctx));
|
||||
}
|
||||
|
||||
void Builder::VisitDirectory(FsFileSystem *fs, BuildDirectoryContext *parent) {
|
||||
FsDir dir;
|
||||
|
||||
/* Get number of child directories. */
|
||||
s64 num_child_dirs = 0;
|
||||
{
|
||||
R_ASSERT(mitm::fs::OpenAtmosphereRomfsDirectory(&dir, this->program_id, parent->path.get(), OpenDirectoryMode_Directory, fs));
|
||||
ON_SCOPE_EXIT { fsDirClose(&dir); };
|
||||
R_ASSERT(fsDirGetEntryCount(&dir, &num_child_dirs));
|
||||
}
|
||||
AMS_ASSERT(num_child_dirs >= 0);
|
||||
|
||||
{
|
||||
BuildDirectoryContext **child_dirs = reinterpret_cast<BuildDirectoryContext **>(std::malloc(sizeof(BuildDirectoryContext *) * num_child_dirs));
|
||||
ON_SCOPE_EXIT { std::free(child_dirs); };
|
||||
AMS_ASSERT(child_dirs != nullptr);
|
||||
s64 cur_child_dir_ind = 0;
|
||||
|
||||
R_ASSERT(mitm::fs::OpenAtmosphereRomfsDirectory(&dir, this->program_id, parent->path.get(), OpenDirectoryMode_All, fs));
|
||||
{
|
||||
ON_SCOPE_EXIT { fsDirClose(&dir); };
|
||||
|
||||
s64 read_entries = 0;
|
||||
while (true) {
|
||||
R_ASSERT(fsDirRead(&dir, &read_entries, 1, &this->dir_entry));
|
||||
if (read_entries != 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
AMS_ASSERT(this->dir_entry.type == FsDirEntryType_Dir || this->dir_entry.type == FsDirEntryType_File);
|
||||
if (this->dir_entry.type == FsDirEntryType_Dir) {
|
||||
BuildDirectoryContext *real_child = nullptr;
|
||||
this->AddDirectory(&real_child, parent, std::make_unique<BuildDirectoryContext>(parent->path.get(), parent->path_len, this->dir_entry.name, strlen(this->dir_entry.name)));
|
||||
AMS_ASSERT(real_child != nullptr);
|
||||
child_dirs[cur_child_dir_ind++] = real_child;
|
||||
AMS_ASSERT(cur_child_dir_ind <= num_child_dirs);
|
||||
} else /* if (this->dir_entry.type == FsDirEntryType_File) */ {
|
||||
this->AddFile(parent, std::make_unique<BuildFileContext>(parent->path.get(), parent->path_len, this->dir_entry.name, strlen(this->dir_entry.name), this->dir_entry.file_size, 0, this->cur_source_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AMS_ASSERT(num_child_dirs == cur_child_dir_ind);
|
||||
for (s64 i = 0; i < num_child_dirs; i++) {
|
||||
this->VisitDirectory(fs, child_dirs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Builder::VisitDirectory(BuildDirectoryContext *parent, u32 parent_offset, const void *dir_table, size_t dir_table_size, const void *file_table, size_t file_table_size) {
|
||||
const DirectoryEntry *parent_entry = GetDirectoryEntry(dir_table, parent_offset);
|
||||
if (parent_entry->file != EmptyEntry) {
|
||||
const FileEntry *cur_file = GetFileEntry(file_table, parent_entry->file);
|
||||
while (true) {
|
||||
this->AddFile(parent, std::make_unique<BuildFileContext>(parent->path.get(), parent->path_len, cur_file->name, cur_file->name_size, cur_file->size, cur_file->offset, this->cur_source_type));
|
||||
if (cur_file->sibling == EmptyEntry) {
|
||||
break;
|
||||
}
|
||||
cur_file = GetFileEntry(file_table, cur_file->sibling);
|
||||
}
|
||||
}
|
||||
if (parent_entry->child != EmptyEntry) {
|
||||
const DirectoryEntry *cur_child = GetDirectoryEntry(dir_table, parent_entry->child);
|
||||
u32 cur_child_offset = parent_entry->child;
|
||||
while (true) {
|
||||
BuildDirectoryContext *real_child = nullptr;
|
||||
this->AddDirectory(&real_child, parent, std::make_unique<BuildDirectoryContext>(parent->path.get(), parent->path_len, cur_child->name, cur_child->name_size));
|
||||
AMS_ASSERT(real_child != nullptr);
|
||||
|
||||
this->VisitDirectory(real_child, cur_child_offset, dir_table, dir_table_size, file_table, file_table_size);
|
||||
|
||||
if (cur_child->sibling == EmptyEntry) {
|
||||
break;
|
||||
}
|
||||
cur_child_offset = cur_child->sibling;
|
||||
cur_child = GetDirectoryEntry(dir_table, cur_child_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Builder::AddSdFiles() {
|
||||
/* Open Sd Card filesystem. */
|
||||
FsFileSystem sd_filesystem;
|
||||
R_ASSERT(fsOpenSdCardFileSystem(&sd_filesystem));
|
||||
ON_SCOPE_EXIT { fsFsClose(&sd_filesystem); };
|
||||
|
||||
this->cur_source_type = DataSourceType::LooseSdFile;
|
||||
this->VisitDirectory(&sd_filesystem, this->root);
|
||||
}
|
||||
|
||||
void Builder::AddStorageFiles(ams::fs::IStorage *storage, DataSourceType source_type) {
|
||||
Header header;
|
||||
R_ASSERT(storage->Read(0, &header, sizeof(Header)));
|
||||
AMS_ASSERT(header.header_size == sizeof(Header));
|
||||
|
||||
/* Read tables. */
|
||||
auto tables = std::unique_ptr<u8[]>(new u8[header.dir_table_size + header.file_table_size]);
|
||||
void *dir_table = tables.get();
|
||||
void *file_table = tables.get() + header.dir_table_size;
|
||||
R_ASSERT(storage->Read(header.dir_table_ofs, dir_table, size_t(header.dir_table_size)));
|
||||
R_ASSERT(storage->Read(header.file_table_ofs, file_table, size_t(header.file_table_size)));
|
||||
|
||||
this->cur_source_type = source_type;
|
||||
this->VisitDirectory(this->root, 0x0, dir_table, size_t(header.dir_table_size), file_table, size_t(header.file_table_size));
|
||||
}
|
||||
|
||||
void Builder::Build(std::vector<SourceInfo> *out_infos) {
|
||||
/* Clear output. */
|
||||
out_infos->clear();
|
||||
|
||||
/* Open an SD card filesystem. */
|
||||
FsFileSystem sd_filesystem;
|
||||
R_ASSERT(fsOpenSdCardFileSystem(&sd_filesystem));
|
||||
ON_SCOPE_EXIT { fsFsClose(&sd_filesystem); };
|
||||
|
||||
/* Calculate hash table sizes. */
|
||||
const size_t num_dir_hash_table_entries = GetHashTableSize(this->num_dirs);
|
||||
const size_t num_file_hash_table_entries = GetHashTableSize(this->num_files);
|
||||
this->dir_hash_table_size = sizeof(u32) * num_dir_hash_table_entries;
|
||||
this->file_hash_table_size = sizeof(u32) * num_file_hash_table_entries;
|
||||
|
||||
/* Allocate metadata, make pointers. */
|
||||
Header *header = reinterpret_cast<Header *>(std::malloc(sizeof(Header)));
|
||||
std::memset(header, 0x00, sizeof(*header));
|
||||
|
||||
const size_t metadata_size = this->dir_hash_table_size + this->dir_table_size + this->file_hash_table_size + this->file_table_size;
|
||||
std::unique_ptr<u8[]> metadata(new u8[metadata_size]);
|
||||
u32 *dir_hash_table = reinterpret_cast<u32 *>(metadata.get());
|
||||
DirectoryEntry *dir_table = reinterpret_cast<DirectoryEntry *>(reinterpret_cast<uintptr_t>(dir_hash_table) + this->dir_hash_table_size);
|
||||
u32 *file_hash_table = reinterpret_cast<u32 *>(reinterpret_cast<uintptr_t>(dir_table) + this->dir_table_size);
|
||||
FileEntry *file_table = reinterpret_cast<FileEntry *>(reinterpret_cast<uintptr_t>(file_hash_table) + this->file_hash_table_size);
|
||||
|
||||
/* Clear hash tables. */
|
||||
static_assert(EmptyEntry == 0xFFFFFFFF);
|
||||
std::memset(dir_hash_table, 0xFF, this->dir_hash_table_size);
|
||||
std::memset(file_hash_table, 0xFF, this->file_hash_table_size);
|
||||
|
||||
/* Emplace metadata source info. */
|
||||
out_infos->emplace_back(0, sizeof(*header), DataSourceType::Memory, header);
|
||||
|
||||
/* Process Files. */
|
||||
{
|
||||
u32 entry_offset = 0;
|
||||
BuildFileContext *cur_file = nullptr;
|
||||
BuildFileContext *prev_file = nullptr;
|
||||
for (const auto &it : this->files) {
|
||||
cur_file = it.second.get();
|
||||
|
||||
/* By default, pad to 0x10 alignment. */
|
||||
this->file_partition_size = util::AlignUp(this->file_partition_size, 0x10);
|
||||
|
||||
/* Check if extra padding is present in original source, preserve it to make our life easier. */
|
||||
const bool is_storage_or_file = cur_file->source_type == DataSourceType::Storage || cur_file->source_type == DataSourceType::File;
|
||||
if (prev_file != nullptr && prev_file->source_type == cur_file->source_type && is_storage_or_file) {
|
||||
const s64 expected = this->file_partition_size - prev_file->offset + prev_file->orig_offset;
|
||||
if (expected != cur_file->orig_offset) {
|
||||
AMS_ASSERT(expected <= cur_file->orig_offset);
|
||||
this->file_partition_size += cur_file->orig_offset - expected;
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculate offsets. */
|
||||
cur_file->offset = this->file_partition_size;
|
||||
this->file_partition_size += cur_file->size;
|
||||
cur_file->entry_offset = entry_offset;
|
||||
entry_offset += sizeof(FileEntry) + util::AlignUp(cur_file->path_len - cur_file->cur_path_ofs, 4);
|
||||
|
||||
/* Save current file as prev for next iteration. */
|
||||
prev_file = cur_file;
|
||||
}
|
||||
/* Assign deferred parent/sibling ownership. */
|
||||
for (auto it = this->files.rbegin(); it != this->files.rend(); it++) {
|
||||
cur_file = it->second.get();
|
||||
cur_file->sibling = cur_file->parent->file;
|
||||
cur_file->parent->file = cur_file;
|
||||
}
|
||||
}
|
||||
|
||||
/* Process Directories. */
|
||||
{
|
||||
u32 entry_offset = 0;
|
||||
BuildDirectoryContext *cur_dir = nullptr;
|
||||
for (const auto &it : this->directories) {
|
||||
cur_dir = it.second.get();
|
||||
cur_dir->entry_offset = entry_offset;
|
||||
entry_offset += sizeof(DirectoryEntry) + util::AlignUp(cur_dir->path_len - cur_dir->cur_path_ofs, 4);
|
||||
}
|
||||
/* Assign deferred parent/sibling ownership. */
|
||||
for (auto it = this->directories.rbegin(); it != this->directories.rend(); it++) {
|
||||
cur_dir = it->second.get();
|
||||
if (cur_dir == this->root) {
|
||||
continue;
|
||||
}
|
||||
cur_dir->sibling = cur_dir->parent->child;
|
||||
cur_dir->parent->child = cur_dir;
|
||||
}
|
||||
}
|
||||
|
||||
/* Populate file tables. */
|
||||
for (const auto &it : this->files) {
|
||||
BuildFileContext *cur_file = it.second.get();
|
||||
FileEntry *cur_entry = GetFileEntry(file_table, cur_file->entry_offset);
|
||||
|
||||
/* Set entry fields. */
|
||||
cur_entry->parent = cur_file->parent->entry_offset;
|
||||
cur_entry->sibling = (cur_file->sibling == nullptr) ? EmptyEntry : cur_file->sibling->entry_offset;
|
||||
cur_entry->offset = cur_file->offset;
|
||||
cur_entry->size = cur_file->size;
|
||||
|
||||
/* Insert into hash table. */
|
||||
const u32 name_size = cur_file->path_len - cur_file->cur_path_ofs;
|
||||
const size_t hash_ind = CalculatePathHash(cur_entry->parent, cur_file->path.get() + cur_file->cur_path_ofs, 0, name_size) % num_file_hash_table_entries;
|
||||
cur_entry->hash = file_hash_table[hash_ind];
|
||||
file_hash_table[hash_ind] = cur_file->entry_offset;
|
||||
|
||||
/* Set name. */
|
||||
cur_entry->name_size = name_size;
|
||||
if (name_size) {
|
||||
std::memset(cur_entry->name + util::AlignDown(name_size, 4), 0, 4);
|
||||
std::memcpy(cur_entry->name, cur_file->path.get() + cur_file->cur_path_ofs, name_size);
|
||||
}
|
||||
|
||||
/* Emplace a source. */
|
||||
switch (cur_file->source_type) {
|
||||
case DataSourceType::Storage:
|
||||
case DataSourceType::File:
|
||||
{
|
||||
/* Try to compact if possible. */
|
||||
auto &back = out_infos->back();
|
||||
if (back.source_type == cur_file->source_type) {
|
||||
back.size = cur_file->offset + FilePartitionOffset + cur_file->size - back.virtual_offset;
|
||||
} else {
|
||||
out_infos->emplace_back(cur_file->offset + FilePartitionOffset, cur_file->size, cur_file->source_type, cur_file->orig_offset + FilePartitionOffset);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DataSourceType::LooseSdFile:
|
||||
{
|
||||
out_infos->emplace_back(cur_file->offset + FilePartitionOffset, cur_file->size, cur_file->source_type, cur_file->path.release());
|
||||
}
|
||||
break;
|
||||
AMS_UNREACHABLE_DEFAULT_CASE();
|
||||
}
|
||||
}
|
||||
|
||||
/* Populate directory tables. */
|
||||
for (const auto &it : this->directories) {
|
||||
BuildDirectoryContext *cur_dir = it.second.get();
|
||||
DirectoryEntry *cur_entry = GetDirectoryEntry(dir_table, cur_dir->entry_offset);
|
||||
|
||||
/* Set entry fields. */
|
||||
cur_entry->parent = cur_dir == this->root ? 0 : cur_dir->parent->entry_offset;
|
||||
cur_entry->sibling = (cur_dir->sibling == nullptr) ? EmptyEntry : cur_dir->sibling->entry_offset;
|
||||
cur_entry->child = (cur_dir->child == nullptr) ? EmptyEntry : cur_dir->child->entry_offset;
|
||||
cur_entry->file = (cur_dir->file == nullptr) ? EmptyEntry : cur_dir->file->entry_offset;
|
||||
|
||||
/* Insert into hash table. */
|
||||
const u32 name_size = cur_dir->path_len - cur_dir->cur_path_ofs;
|
||||
const size_t hash_ind = CalculatePathHash(cur_entry->parent, cur_dir->path.get() + cur_dir->cur_path_ofs, 0, name_size) % num_dir_hash_table_entries;
|
||||
cur_entry->hash = dir_hash_table[hash_ind];
|
||||
dir_hash_table[hash_ind] = cur_dir->entry_offset;
|
||||
|
||||
/* Set name. */
|
||||
cur_entry->name_size = name_size;
|
||||
if (name_size) {
|
||||
std::memset(cur_entry->name + util::AlignDown(name_size, 4), 0, 4);
|
||||
std::memcpy(cur_entry->name, cur_dir->path.get() + cur_dir->cur_path_ofs, name_size);
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete maps. */
|
||||
this->root = nullptr;
|
||||
this->directories.clear();
|
||||
this->files.clear();
|
||||
|
||||
/* Set header fields. */
|
||||
header->header_size = sizeof(*header);
|
||||
header->file_hash_table_size = this->file_hash_table_size;
|
||||
header->file_table_size = this->file_table_size;
|
||||
header->dir_hash_table_size = this->dir_hash_table_size;
|
||||
header->dir_table_size = this->dir_table_size;
|
||||
header->file_partition_ofs = FilePartitionOffset;
|
||||
header->dir_hash_table_ofs = util::AlignUp(FilePartitionOffset + this->file_partition_size, 4);
|
||||
header->dir_table_ofs = header->dir_hash_table_ofs + header->dir_hash_table_size;
|
||||
header->file_hash_table_ofs = header->dir_table_ofs + header->dir_table_size;
|
||||
header->file_table_ofs = header->file_hash_table_ofs + header->file_hash_table_size;
|
||||
|
||||
/* Save metadata to the SD card, to save on memory space. */
|
||||
{
|
||||
FsFile metadata_file;
|
||||
R_ASSERT(mitm::fs::SaveAtmosphereSdFile(&metadata_file, this->program_id, "romfs_metadata.bin", metadata.get(), metadata_size));
|
||||
out_infos->emplace_back(header->dir_hash_table_ofs, metadata_size, DataSourceType::Metadata, new RemoteFile(metadata_file));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
220
stratosphere/ams_mitm/source/fs_mitm/fsmitm_romfs.hpp
Normal file
220
stratosphere/ams_mitm/source/fs_mitm/fsmitm_romfs.hpp
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stratosphere.hpp>
|
||||
|
||||
namespace ams::mitm::fs::romfs {
|
||||
|
||||
enum class DataSourceType {
|
||||
Storage,
|
||||
File,
|
||||
LooseSdFile,
|
||||
Metadata,
|
||||
Memory,
|
||||
};
|
||||
|
||||
struct SourceInfo {
|
||||
s64 virtual_offset;
|
||||
s64 size;
|
||||
union {
|
||||
struct {
|
||||
s64 offset;
|
||||
} storage_source_info;
|
||||
struct {
|
||||
s64 offset;
|
||||
} file_source_info;
|
||||
struct {
|
||||
char *path;
|
||||
} loose_source_info;
|
||||
struct {
|
||||
ams::fs::fsa::IFile *file;
|
||||
} metadata_source_info;
|
||||
struct {
|
||||
u8 *data;
|
||||
} memory_source_info;
|
||||
};
|
||||
DataSourceType source_type;
|
||||
bool cleaned_up;
|
||||
|
||||
SourceInfo(s64 v_o, s64 sz, DataSourceType type, s64 p_o) : virtual_offset(v_o), size(sz), source_type(type), cleaned_up(false) {
|
||||
switch (this->source_type) {
|
||||
case DataSourceType::Storage:
|
||||
this->storage_source_info.offset = p_o;
|
||||
break;
|
||||
case DataSourceType::File:
|
||||
this->file_source_info.offset = p_o;
|
||||
break;
|
||||
AMS_UNREACHABLE_DEFAULT_CASE();
|
||||
}
|
||||
}
|
||||
|
||||
SourceInfo(s64 v_o, s64 sz, DataSourceType type, void *arg) : virtual_offset(v_o), size(sz), source_type(type), cleaned_up(false) {
|
||||
switch (this->source_type) {
|
||||
case DataSourceType::LooseSdFile:
|
||||
this->loose_source_info.path = static_cast<char *>(arg);
|
||||
break;
|
||||
case DataSourceType::Metadata:
|
||||
this->metadata_source_info.file = static_cast<ams::fs::fsa::IFile *>(arg);
|
||||
break;
|
||||
case DataSourceType::Memory:
|
||||
this->memory_source_info.data = static_cast<u8 *>(arg);
|
||||
break;
|
||||
AMS_UNREACHABLE_DEFAULT_CASE();
|
||||
}
|
||||
}
|
||||
|
||||
void Cleanup() {
|
||||
AMS_ASSERT(!this->cleaned_up);
|
||||
this->cleaned_up = true;
|
||||
|
||||
switch (this->source_type) {
|
||||
case DataSourceType::Storage:
|
||||
case DataSourceType::File:
|
||||
break;
|
||||
case DataSourceType::Metadata:
|
||||
delete this->metadata_source_info.file;
|
||||
break;
|
||||
case DataSourceType::LooseSdFile:
|
||||
delete[] this->loose_source_info.path;
|
||||
break;
|
||||
case DataSourceType::Memory:
|
||||
std::free(static_cast<void *>(this->memory_source_info.data));
|
||||
break;
|
||||
AMS_UNREACHABLE_DEFAULT_CASE();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constexpr inline bool operator<(const SourceInfo &lhs, const SourceInfo &rhs) {
|
||||
return lhs.virtual_offset < rhs.virtual_offset;
|
||||
}
|
||||
|
||||
constexpr inline bool operator<(const SourceInfo &lhs, const s64 rhs) {
|
||||
return lhs.virtual_offset <= rhs;
|
||||
}
|
||||
|
||||
struct BuildFileContext;
|
||||
|
||||
struct BuildDirectoryContext {
|
||||
NON_COPYABLE(BuildDirectoryContext);
|
||||
NON_MOVEABLE(BuildDirectoryContext);
|
||||
|
||||
std::unique_ptr<char[]> path;
|
||||
BuildDirectoryContext *parent;
|
||||
BuildDirectoryContext *child;
|
||||
BuildDirectoryContext *sibling;
|
||||
BuildFileContext *file;
|
||||
u32 cur_path_ofs;
|
||||
u32 path_len;
|
||||
u32 entry_offset;
|
||||
|
||||
struct RootTag{};
|
||||
|
||||
BuildDirectoryContext(RootTag) : parent(nullptr), child(nullptr), sibling(nullptr), file(nullptr), cur_path_ofs(0), path_len(0), entry_offset(0) {
|
||||
this->path = std::make_unique<char[]>(1);
|
||||
}
|
||||
|
||||
BuildDirectoryContext(const char *parent_path, size_t parent_path_len, const char *entry_name, size_t entry_name_len) : parent(nullptr), child(nullptr), sibling(nullptr), file(nullptr), entry_offset(0) {
|
||||
this->cur_path_ofs = parent_path_len + 1;
|
||||
this->path_len = this->cur_path_ofs + entry_name_len;
|
||||
this->path = std::unique_ptr<char[]>(new char[this->path_len + 1]);
|
||||
std::memcpy(this->path.get(), parent_path, parent_path_len);
|
||||
this->path[parent_path_len] = '/';
|
||||
std::memcpy(this->path.get() + parent_path_len + 1, entry_name, entry_name_len);
|
||||
this->path[this->path_len] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct BuildFileContext {
|
||||
NON_COPYABLE(BuildFileContext);
|
||||
NON_MOVEABLE(BuildFileContext);
|
||||
|
||||
std::unique_ptr<char[]> path;
|
||||
BuildDirectoryContext *parent;
|
||||
BuildFileContext *sibling;
|
||||
s64 offset;
|
||||
s64 size;
|
||||
s64 orig_offset;
|
||||
u32 cur_path_ofs;
|
||||
u32 path_len;
|
||||
u32 entry_offset;
|
||||
DataSourceType source_type;
|
||||
|
||||
BuildFileContext(const char *parent_path, size_t parent_path_len, const char *entry_name, size_t entry_name_len, s64 sz, s64 o_o, DataSourceType type) : parent(nullptr), sibling(nullptr), offset(0), size(sz), orig_offset(o_o), entry_offset(0), source_type(type) {
|
||||
this->cur_path_ofs = parent_path_len + 1;
|
||||
this->path_len = this->cur_path_ofs + entry_name_len;
|
||||
this->path = std::unique_ptr<char[]>(new char[this->path_len + 1]);
|
||||
std::memcpy(this->path.get(), parent_path, parent_path_len);
|
||||
this->path[parent_path_len] = '/';
|
||||
std::memcpy(this->path.get() + parent_path_len + 1, entry_name, entry_name_len);
|
||||
this->path[this->path_len] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct Builder {
|
||||
NON_COPYABLE(Builder);
|
||||
NON_MOVEABLE(Builder);
|
||||
private:
|
||||
struct PathCompare {
|
||||
static constexpr inline int Compare(const char *a, const char *b) {
|
||||
unsigned char c1{}, c2{};
|
||||
while ((c1 = *a++) == (c2 = *b++)) {
|
||||
if (c1 == '\x00') {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return (c1 - c2);
|
||||
}
|
||||
|
||||
constexpr bool operator()(const char *a, const char *b) const {
|
||||
return PathCompare::Compare(a, b) < 0;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using PathMap = std::map<const char *, std::unique_ptr<T>, PathCompare>;
|
||||
private:
|
||||
ncm::ProgramId program_id;
|
||||
BuildDirectoryContext *root;
|
||||
PathMap<BuildDirectoryContext> directories;
|
||||
PathMap<BuildFileContext> files;
|
||||
size_t num_dirs;
|
||||
size_t num_files;
|
||||
size_t dir_table_size;
|
||||
size_t file_table_size;
|
||||
size_t dir_hash_table_size;
|
||||
size_t file_hash_table_size;
|
||||
size_t file_partition_size;
|
||||
|
||||
ams::fs::DirectoryEntry dir_entry;
|
||||
DataSourceType cur_source_type;
|
||||
private:
|
||||
void VisitDirectory(FsFileSystem *fs, BuildDirectoryContext *parent);
|
||||
void VisitDirectory(BuildDirectoryContext *parent, u32 parent_offset, const void *dir_table, size_t dir_table_size, const void *file_table, size_t file_table_size);
|
||||
|
||||
void AddDirectory(BuildDirectoryContext **out, BuildDirectoryContext *parent_ctx, std::unique_ptr<BuildDirectoryContext> file_ctx);
|
||||
void AddFile(BuildDirectoryContext *parent_ctx, std::unique_ptr<BuildFileContext> file_ctx);
|
||||
public:
|
||||
Builder(ncm::ProgramId pr_id);
|
||||
|
||||
void AddSdFiles();
|
||||
void AddStorageFiles(ams::fs::IStorage *storage, DataSourceType source_type);
|
||||
|
||||
void Build(std::vector<SourceInfo> *out_infos);
|
||||
};
|
||||
|
||||
}
|
@ -291,7 +291,7 @@ namespace ams::settings::fwdbg {
|
||||
Result LoadSdCardKeyValueStore() {
|
||||
/* Open file. */
|
||||
FsFile config_file;
|
||||
if (R_FAILED(ams::mitm::fs::OpenAtmosphereSdFile(&config_file, "/system_settings.ini", FsOpenMode_Read))) {
|
||||
if (R_FAILED(ams::mitm::fs::OpenAtmosphereSdFile(&config_file, "/system_settings.ini", fs::OpenMode_Read))) {
|
||||
/* It's okay if the file isn't readable/present, because we already loaded defaults. */
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ include $(DEVKITPRO)/libnx/switch_rules
|
||||
# INCLUDES is a list of directories containing header files
|
||||
#---------------------------------------------------------------------------------
|
||||
TARGET := $(notdir $(CURDIR))
|
||||
SOURCES := source source/ams source/hos source/result source/os source/os/impl source/dd source/sf source/sf/cmif source/sf/hipc source/dmnt source/spl source/spl/smc source/updater source/patcher source/map source/rnd source/util source/sm source/cfg source/pm source/hid source/ldr source/kvdb source/settings source/boot2
|
||||
SOURCES := source source/ams source/hos source/result source/os source/os/impl source/dd source/fs source/sf source/sf/cmif source/sf/hipc source/dmnt source/spl source/spl/smc source/updater source/patcher source/map source/rnd source/util source/sm source/cfg source/pm source/hid source/ldr source/kvdb source/settings source/boot2
|
||||
DATA := data
|
||||
INCLUDES := include
|
||||
|
||||
|
@ -22,4 +22,5 @@
|
||||
#include "fs/fs_remote_filesystem.hpp"
|
||||
#include "fs/fs_istorage.hpp"
|
||||
#include "fs/fs_remote_storage.hpp"
|
||||
#include "fs/fs_file_storage.hpp"
|
||||
#include "fs/fs_query_range.hpp"
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "fs_common.hpp"
|
||||
#include "fs_istorage.hpp"
|
||||
#include "fsa/fs_ifile.hpp"
|
||||
|
||||
namespace ams::fs {
|
||||
|
||||
class FileStorage : public IStorage {
|
||||
private:
|
||||
static constexpr s64 InvalidSize = -1;
|
||||
private:
|
||||
std::unique_ptr<fsa::IFile> unique_file;
|
||||
std::shared_ptr<fsa::IFile> shared_file;
|
||||
fsa::IFile *base_file;
|
||||
s64 size;
|
||||
public:
|
||||
FileStorage(fsa::IFile *f) : unique_file(f), size(InvalidSize) {
|
||||
this->base_file = this->unique_file.get();
|
||||
}
|
||||
|
||||
FileStorage(std::unique_ptr<fsa::IFile> f) : unique_file(std::move(f)), size(InvalidSize) {
|
||||
this->base_file = this->unique_file.get();
|
||||
}
|
||||
|
||||
FileStorage(std::shared_ptr<fsa::IFile> f) : shared_file(f), size(InvalidSize) {
|
||||
this->base_file = this->shared_file.get();
|
||||
}
|
||||
|
||||
virtual ~FileStorage() { /* ... */ }
|
||||
protected:
|
||||
Result UpdateSize();
|
||||
public:
|
||||
virtual Result Read(s64 offset, void *buffer, size_t size) override;
|
||||
virtual Result Write(s64 offset, const void *buffer, size_t size) override;
|
||||
virtual Result Flush() override;
|
||||
virtual Result GetSize(s64 *out_size) override;
|
||||
virtual Result SetSize(s64 size) override;
|
||||
virtual Result OperateRange(void *dst, size_t dst_size, OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) override;
|
||||
};
|
||||
|
||||
}
|
93
stratosphere/libstratosphere/source/fs/fs_file_storage.cpp
Normal file
93
stratosphere/libstratosphere/source/fs/fs_file_storage.cpp
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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/>.
|
||||
*/
|
||||
#include <stratosphere.hpp>
|
||||
|
||||
namespace ams::fs {
|
||||
|
||||
Result FileStorage::UpdateSize() {
|
||||
R_UNLESS(this->size == InvalidSize, ResultSuccess());
|
||||
return this->base_file->GetSize(&this->size);
|
||||
}
|
||||
|
||||
Result FileStorage::Read(s64 offset, void *buffer, size_t size) {
|
||||
/* Immediately succeed if there's nothing to read. */
|
||||
R_UNLESS(size > 0, ResultSuccess());
|
||||
|
||||
/* Validate buffer. */
|
||||
R_UNLESS(buffer != nullptr, fs::ResultNullptrArgument());
|
||||
|
||||
/* Ensure our size is valid. */
|
||||
R_TRY(this->UpdateSize());
|
||||
|
||||
/* Ensure our access is valid. */
|
||||
R_UNLESS(IStorage::IsRangeValid(offset, size, this->size), fs::ResultOutOfRange());
|
||||
|
||||
size_t read_size;
|
||||
return this->base_file->Read(&read_size, offset, buffer, size);
|
||||
}
|
||||
|
||||
Result FileStorage::Write(s64 offset, const void *buffer, size_t size) {
|
||||
/* Immediately succeed if there's nothing to write. */
|
||||
R_UNLESS(size > 0, ResultSuccess());
|
||||
|
||||
/* Validate buffer. */
|
||||
R_UNLESS(buffer != nullptr, fs::ResultNullptrArgument());
|
||||
|
||||
/* Ensure our size is valid. */
|
||||
R_TRY(this->UpdateSize());
|
||||
|
||||
/* Ensure our access is valid. */
|
||||
R_UNLESS(IStorage::IsRangeValid(offset, size, this->size), fs::ResultOutOfRange());
|
||||
|
||||
return this->base_file->Write(offset, buffer, size, fs::WriteOption());
|
||||
}
|
||||
|
||||
Result FileStorage::Flush() {
|
||||
return this->base_file->Flush();
|
||||
}
|
||||
|
||||
Result FileStorage::GetSize(s64 *out_size) {
|
||||
R_TRY(this->UpdateSize());
|
||||
*out_size = this->size;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result FileStorage::SetSize(s64 size) {
|
||||
this->size = InvalidSize;
|
||||
return this->base_file->SetSize(size);
|
||||
}
|
||||
|
||||
Result FileStorage::OperateRange(void *dst, size_t dst_size, OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) {
|
||||
switch (op_id) {
|
||||
case OperationId::InvalidateCache:
|
||||
case OperationId::QueryRange:
|
||||
if (size == 0) {
|
||||
if (op_id == OperationId::QueryRange) {
|
||||
R_UNLESS(dst != nullptr, fs::ResultNullptrArgument());
|
||||
R_UNLESS(dst_size == sizeof(QueryRangeInfo), fs::ResultInvalidSize());
|
||||
reinterpret_cast<QueryRangeInfo *>(dst)->Clear();
|
||||
}
|
||||
return ResultSuccess();
|
||||
}
|
||||
R_TRY(this->UpdateSize());
|
||||
R_UNLESS(IStorage::IsOffsetAndSizeValid(offset, size), fs::ResultOutOfRange());
|
||||
return this->base_file->OperateRange(dst, dst_size, op_id, offset, size, src, src_size);
|
||||
default:
|
||||
return fs::ResultUnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user