mikage-dev/source/processes/fs.cpp
2024-09-29 16:18:56 +02:00

2846 lines
125 KiB
C++

#include "fs.hpp"
#include "fs_common.hpp"
#include "fs_paths.hpp"
#include <platform/file_formats/ncch.hpp>
#include "platform/pxi.hpp"
#include "platform/sm.hpp"
#include "../os.hpp"
#include "../os_serialization.hpp"
#include <framework/exceptions.hpp>
#include <boost/algorithm/cxx11/all_of.hpp>
#include <boost/range/irange.hpp>
#include <boost/range/adaptor/reversed.hpp>
#include <boost/range/adaptor/sliced.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/algorithm/copy.hpp>
#include <boost/range/algorithm/for_each.hpp>
#include <boost/range/numeric.hpp>
#include <range/v3/numeric.hpp>
#include <range/v3/algorithm/copy.hpp>
#include <range/v3/algorithm/find.hpp>
#include <range/v3/algorithm/transform.hpp>
#include <range/v3/iterator/insert_iterators.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/take.hpp>
#include <codecvt>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
using namespace Platform::FS;
namespace HLE {
// TODO: Factor out the state in FakeFS into a separate state for this
using FSContext = FakeFS;
using OS::HandlePrinter;
using OS::ThreadPrinter;
using OS::HANDLE_INVALID;
using OS::WrappedFakeThread;
using OS::ClientSession;
using OS::ServerSession;
using OS::Thread;
static std::filesystem::path GetRootDataDirectory() {
return "./data/";
}
// TODO: Steel diver fails to boot if this directory doesn't exist - should make sure to create it automatically!
static std::filesystem::path HostSdmcDirectory() {
auto path_str = "./data/sdmc";
return std::filesystem::path(path_str);
}
/**
* FileBuffer that refers to emulated memory
*/
class FileBufferInEmulatedMemory : public FileBuffer {
public:
FakeThread& thread;
uint32_t address;
void Write(char* source, uint32_t num_bytes) override {
auto WriteToAddr = [this](auto&& addr, auto&& value) { thread.WriteMemory(addr, value); return addr + 1; };
ranges::v3::accumulate(source, source + num_bytes, address, WriteToAddr);
}
FileBufferInEmulatedMemory(FakeThread& thread, uint32_t address) : thread(thread), address(address) {
}
};
void File::Fail() {
throw std::runtime_error(std::string{"Unimplemented file operation for \""} + GetFileName() + "\"");
}
OS::OS::ResultAnd<uint32_t> File::Read(FileContext&, FSContext&, uint64_t offset, uint32_t num_bytes, FileBuffer&) {
Fail();
return std::make_tuple(RESULT_OK, 0);
}
OS::OS::ResultAnd<uint32_t> File::Write(FakeThread& thread, FSContext&, uint64_t offset, uint32_t num_bytes, uint32_t options, uint32_t buffer_addr) {
Fail();
return std::make_tuple(RESULT_OK, 0);
}
class RomfsFile : public File {
ProgramInfo program_info;
std::ifstream ncch;
std::ifstream::pos_type romfs_start;
uint32_t romfs_size;
std::string filename;
public:
RomfsFile(ProgramInfo& program_info) : program_info(program_info) {
if (program_info.media_type == 0) {
// TODO: The content ID is hardcoded currently.
uint32_t content_id = 0;
filename = [&]() -> std::string {
std::stringstream filename;
filename << "data/";
filename << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id >> 32);
filename << "/";
filename << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id & 0xFFFFFFFF);
filename << "/content/";
filename << std::hex << std::setw(8) << std::setfill('0') << content_id;
filename << ".cxi";
return filename.str();
}();
ncch.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit);
ncch.open(filename);
auto ncch_start = ncch.tellg();
ncch.seekg(ncch_start + static_cast<std::ifstream::off_type>(offsetof(FileFormat::NCCHHeader,romfs_offset)));
decltype(FileFormat::NCCHHeader::romfs_offset) offset;
decltype(FileFormat::NCCHHeader::romfs_offset) size;
ncch.read(reinterpret_cast<char*>(&offset), sizeof(offset));
ncch.read(reinterpret_cast<char*>(&size), sizeof(size));
auto ivfc_start = ncch_start + static_cast<std::ifstream::off_type>(offset.ToBytes());
/* std::cerr << "ivfc offset: 0x" << std::hex << ivfc_start-ncch_start << std::endl;
ncch.seekg(ivfc_start + static_cast<std::ifstream::off_type>(0x3c)); // offset in IVFC to level 3 entry offset
boost::endian::little_uint32_t level3_offset;
ncch.read(reinterpret_cast<char*>(&level3_offset), sizeof(level3_offset));
std::cerr << "level3 offset: 0x" << std::hex << level3_offset << std::endl;*/
// romfs_start = ivfc_start + static_cast<std::ifstream::off_type>(level3_offset);
romfs_start = ivfc_start + static_cast<std::ifstream::off_type>(0x1000);
romfs_size = size.ToBytes();
} else if (program_info.media_type == 2) {
throw std::runtime_error("TODO.");
} else {
throw std::runtime_error("Unknown media type");
}
}
// Returns result code and number of bytes read (0 on error)
OS::OS::ResultAnd<uint32_t> Read(FileContext&, FSContext&, uint64_t offset, uint32_t num_bytes, FileBuffer& buffer) override {
ncch.seekg(romfs_start + static_cast<std::ifstream::off_type>(offset));
if (offset + num_bytes > romfs_size) {
throw std::runtime_error(fmt::format("Read end address {:#x} exceeds RomFS size {:#x}", offset + num_bytes, romfs_size));
}
std::vector<char> data(num_bytes);
ncch.read(data.data(), num_bytes);
buffer.Write(data.data(), num_bytes);
/* std::stringstream ss;
for (uint32_t off = 0; off < num_bytes; ++off)
ss << std::setw(2) << std::hex << std::setfill('0') << static_cast<uint32_t>(static_cast<uint8_t>(thread.ReadMemory(buffer_addr + off)));
std::cerr << ss.str() << std::endl;*/
return std::make_tuple(RESULT_OK, num_bytes);
}
virtual OS::OS::ResultAnd<uint32_t> Write(FakeThread& thread, FSContext&, uint64_t offset, uint32_t num_bytes, uint32_t options, uint32_t buffer_addr) override {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
return std::make_tuple(RESULT_OK, 0 /* TODO: Value of bytes written */);
}
virtual const char* GetFileName() override {
static std::string buffer;
buffer = filename + "(romfs)";
return buffer.c_str();
}
};
class FileSaveData : public File {
std::fstream output_file;
uint64_t size = 0;
// If true, Write()ing past the file size will implicitly resize the file
// If false, Write will discard any input data exceeding the file size
bool implicit_resize = false;
public:
/**
* @param host_savegame_path Path to the savegame directory on the host
* @param internal_path Path to the file within the savegame tree, starting with '/'
*/
FileSaveData(FSContext& context, const ValidatedHostPath& path, bool create, bool implicit_resize)
: implicit_resize(implicit_resize) {
if (!std::filesystem::exists(path.path.parent_path())) {
context.logger.info("Parent directory {} does not exist, returning error", path.path.parent_path());
throw IPC::IPCError { 0, 0xc8804471 };
}
output_file.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit);
if (!std::filesystem::exists(path)) {
if (create) {
context.logger.info("Implicitly creating file at {}", path.path);
// Create the file by opening and then closing it. This is done in a separate step so that we don't "accidentally" end up in append mode
output_file.open(path.path, std::ios::out);
output_file.close();
} else {
throw IPC::IPCError { 0, 0xc8804470 };
}
}
if (!std::filesystem::is_regular_file(path)) {
test_mode ? throw IPC::IPCError { 0, 0xe0c04702 }
: throw std::runtime_error(fmt::format("Attempted to create FileSaveData from path {} that is not a file", path.path));
}
output_file.open(path.path, std::ios::binary | std::ios::out | std::ios::in);
// Get file size
output_file.seekg(0, std::ios::beg);
auto begin = output_file.tellg();
output_file.seekg(0, std::ios::end);
size = output_file.tellg() - begin;
}
// Returns result code and number of bytes read (0 on error)
OS::OS::ResultAnd<uint32_t> Read(FileContext& context, FSContext&, uint64_t offset, uint32_t num_bytes, FileBuffer& buffer) override {
if (offset > size) {
throw std::runtime_error("Attempted to seek past end of file");
}
if (num_bytes == 0) {
throw std::runtime_error("Tried to read 0 bytes. Not invalid, but indicates an emulator bug");
}
output_file.seekg(offset, std::ios_base::beg);
std::vector<char> data(num_bytes);
try {
output_file.read(data.data(), num_bytes);
} catch (std::ios_base::failure& err) {
if (test_mode) {
context.logger.warn("Failure during file read due to ios exception: {}", err.what());
// Ignore error and return number of bytes we managed to read
output_file.clear();
} else {
throw std::runtime_error(fmt::format("Failed to read data from file: {}", err.what()));
}
}
auto bytes_read = output_file.gcount();
buffer.Write(data.data(), bytes_read);
// NOTE: When only parts of the requested data could be read, the result code will still indicate success
return std::make_tuple(RESULT_OK, bytes_read);
}
OS::OS::ResultAnd<uint32_t> Write(FakeThread& thread, FSContext&, uint64_t offset, uint32_t num_bytes, uint32_t options, uint32_t buffer_addr) override {
if (offset > size) {
if (test_mode) {
// The write cursor must always start within the bounds of the file (error 0xe0e046ca),
// but for non-resizeable files error 0xe0e046c1 takes precedence
return std::make_tuple<Result, uint32_t>(implicit_resize ? 0xe0e046ca : 0xe0e046c1, 0);
} else {
throw std::runtime_error("Attempted to seek past end of file");
}
}
// TODO: Bravely Default has been found to use options 0x10001. Figure out what the actual set of supported options is
if (!implicit_resize && (options != 0 && options != 0x10001)) {
return test_mode ? std::make_tuple<Result, uint32_t>(0xe0e046c1, 0)
: throw std::runtime_error("May not use non-zero options for non-implicitly resizeable file");
}
if (num_bytes == 0) {
// NOTE: mset performs a zero-length write to idb.dat during initial system setup after setting the time
return std::make_tuple(RESULT_OK, uint32_t { 0 });
}
output_file.seekp(offset, std::ios_base::beg);
if (!implicit_resize && offset + num_bytes > size) {
if (!test_mode) {
throw std::runtime_error("Writing past end of non-implicitly resizable file");
} else {
num_bytes = size - offset;
}
}
auto DataAddressRange = boost::irange(buffer_addr, buffer_addr + num_bytes);
auto ReadFromAddr = std::bind(&FakeThread::ReadMemory, &thread, std::placeholders::_1);
std::vector<char> data(num_bytes);
boost::copy(DataAddressRange | boost::adaptors::transformed(ReadFromAddr), data.begin());
output_file.write(data.data(), data.size());
output_file.flush();
if (size < offset + num_bytes)
size = offset + num_bytes;
return std::make_tuple(RESULT_OK, num_bytes);
}
OS::OS::ResultAnd<uint64_t> GetSize(FakeThread& thread, FSContext&) override {
return std::make_tuple(RESULT_OK, size);
}
OS::OS::ResultAnd<> SetSize(FakeThread& thread, FSContext&, uint64_t size) override {
if (size < this->size) {
// Home Menu workaround
// throw std::runtime_error(fmt::format("Reducing file size not supported, yet (current size: {:#x}, requested {:#x})",
// this->size, size));
} else if (size != this->size) {
output_file.seekp(0, std::ios_base::end);
std::vector<char> zeroes(64, 0);
const uint64_t bytes_to_append = size - this->size;
for (uint64_t bytes_left = bytes_to_append; bytes_left > 0;) {
auto chunk_size = std::min<uint64_t>(64, bytes_left);
output_file.write(zeroes.data(), chunk_size);
bytes_left -= chunk_size;
}
this->size = size;
}
return std::make_tuple(RESULT_OK);
}
};
class GenericHostFile : public File {
std::fstream output_file;
uint64_t size = 0;
public:
static constexpr Result result_file_not_found = 0xc8804478;
// Same as for files
static constexpr Result result_directory_not_found = 0xc8804478;
static constexpr Result result_file_already_exists = 0xc82044be;
static constexpr Result result_directory_already_exists = 0xc82044be;
/// @param sdmc_path Full path to the SDMC file on the host filesystem
GenericHostFile(const ValidatedHostPath& path, bool create) {
if (!std::filesystem::exists(path)) {
if (create) {
// Create the file by opening and then closing it. This is done in a separate step so that we don't "accidentally" end up in append mode
output_file.open(path.path, std::ios::out);
output_file.close();
} else {
throw IPC::IPCError { 0, result_file_not_found };
}
}
output_file.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit);
output_file.open(path.path, std::ios::binary | std::ios::out | std::ios::in);
auto begin = output_file.tellg();
output_file.seekg(0, std::fstream::end);
size = output_file.tellg() - begin;
output_file.seekg(0, std::fstream::beg);
}
// Returns result code and number of bytes read (0 on error)
OS::OS::ResultAnd<uint32_t> Read(FileContext&, FSContext&, uint64_t offset, uint32_t num_bytes, FileBuffer& buffer) override {
output_file.seekg(offset, std::ios_base::beg);
std::vector<char> data(num_bytes);
output_file.read(data.data(), num_bytes);
buffer.Write(data.data(), num_bytes);
return std::make_tuple(RESULT_OK, num_bytes);
}
OS::OS::ResultAnd<uint32_t> Write(FakeThread& thread, FSContext&, uint64_t offset, uint32_t num_bytes, uint32_t /*options*/, uint32_t buffer_addr) override {
output_file.seekp(offset, std::ios_base::beg);
auto DataAddressRange = boost::irange(buffer_addr, buffer_addr + num_bytes);
auto ReadFromAddr = std::bind(&FakeThread::ReadMemory, &thread, std::placeholders::_1);
std::vector<char> data(num_bytes);
boost::copy(DataAddressRange | boost::adaptors::transformed(ReadFromAddr), data.begin());
output_file.write(data.data(), data.size());
output_file.flush();
return std::make_tuple(RESULT_OK, num_bytes);
}
OS::OS::ResultAnd<uint64_t> GetSize(FakeThread&, FSContext&) override {
return std::make_tuple(RESULT_OK, size);
}
OS::OS::ResultAnd<> SetSize(FakeThread&, FSContext&, uint64_t new_size) override {
this->size = new_size;
return std::make_tuple(RESULT_OK);
}
};
class FilePXI : public File {
uint64_t handle;
public:
FilePXI(uint64_t file_handle) : handle(file_handle) {
}
OS::OS::ResultAnd<uint32_t> Read(FileContext&, FSContext& context, uint64_t offset, uint32_t num_bytes, FileBuffer& buffer) override {
// TODO: Turn PXI indirection into an interface that can be mocked for frontend access. For now, this function will only work with FileBufferInEmulatedMemory
auto& concrete_buffer = dynamic_cast<FileBufferInEmulatedMemory&>(buffer);
// Expose the user-provided address through a PXI buffer descriptor and forward this call to PXIFS
auto static_buffer_info = IPC::StaticBuffer{ concrete_buffer.address, num_bytes, 0 }; // TODO: Do we need to specify the data size here or the actual static buffer size?
auto size = IPC::SendIPCRequest<Platform::PXI::FS::ReadFile>(concrete_buffer.thread, context.pxifs_session, handle, offset, num_bytes, static_buffer_info);
return std::make_tuple(RESULT_OK, size);
}
// OS::OS::ResultAnd<uint32_t> Write(FakeThread& thread, uint64_t offset, uint32_t num_bytes, uint32_t options, uint32_t buffer_addr) override {
// output_file.seekp(offset, std::ios_base::beg);
//
// auto DataAddressRange = boost::irange(buffer_addr, buffer_addr + num_bytes);
// auto ReadFromAddr = std::bind(&FakeThread::ReadMemory, &thread, std::placeholders::_1);
// std::vector<char> data(num_bytes);
// boost::copy(DataAddressRange | boost::adaptors::transformed(ReadFromAddr), data.begin());
// std::cerr << "Writing " << std::dec << num_bytes << " bytes to offset " << std::dec << offset << ": ";
// boost::copy(data, std::ostream_iterator<char>(std::cerr));
// std::cerr << std::endl;
//
// output_file.write(data.data(), data.size());
// output_file.flush();
//
// return std::make_tuple(RESULT_OK, num_bytes);
// }
//
OS::OS::ResultAnd<uint64_t> GetSize(FakeThread& thread, FSContext& context) override {
auto size = IPC::SendIPCRequest<Platform::PXI::FS::GetFileSize>(thread, context.pxifs_session, handle);
return std::make_tuple(RESULT_OK, size);
}
OS::OS::ResultAnd<> SetSize(FakeThread& thread, FSContext& context, uint64_t size) override {
IPC::SendIPCRequest<Platform::PXI::FS::SetFileSize>(thread, context.pxifs_session, size, handle);
return std::make_tuple(RESULT_OK);
}
};
/**
* Represents a file that has been closed but that's still accessible via a session.
* FakeFS will replace File instances with instances of this class upon receiving a Close() IPC request
*/
class FileClosed : public File {
OS::OS::ResultAnd<uint32_t> Read(FileContext&, FakeFS&, uint64_t, uint32_t, FileBuffer&) override {
return test_mode ? std::make_pair<Result, uint32_t>(0xc8a044dc, 0)
: throw std::runtime_error("Attempting to Read from closed file");
}
OS::OS::ResultAnd<uint32_t> Write(FakeThread&, FakeFS&, uint64_t, uint32_t, uint32_t, uint32_t) override {
return test_mode ? std::make_pair<Result, uint32_t>(0xc8a044dc, 0)
: throw std::runtime_error("Attempting to Write to closed file");
}
OS::OS::ResultAnd<uint64_t> GetSize(FakeThread&, FakeFS&) override {
return test_mode ? std::make_pair<Result, uint64_t>(0xc8a044dc, 0)
: throw std::runtime_error("Attempting to GetSize of closed file");
}
OS::OS::ResultAnd<> SetSize(FakeThread&, FakeFS&, uint64_t) override {
return test_mode ? std::make_tuple<Result>(0xc8a044dc)
: throw std::runtime_error("Attempting to SetSize of closed file");
}
};
class SubFile : public File {
std::shared_ptr<File> file;
uint64_t offset;
uint64_t num_bytes;
public:
SubFile(std::shared_ptr<File> file, uint64_t offset, uint64_t num_bytes) : file(file), offset(offset), num_bytes(num_bytes) {
ValidateContract(!file->has_subfile);
file->has_subfile = true;
}
~SubFile() {
file->has_subfile = false;
}
OS::OS::ResultAnd<uint32_t> Read(FileContext& file_context, FakeFS& context, uint64_t subfile_offset, uint32_t subfile_num_bytes, FileBuffer& target) override {
if (subfile_offset + subfile_num_bytes > num_bytes) {
throw Mikage::Exceptions::Invalid("Attempted to read past sub file boundaries");
}
return file->Read(file_context, context, offset + subfile_offset, subfile_num_bytes, target);
}
};
bool File::HasOpenSubFile() const noexcept {
return has_subfile;
}
// TODO.
class Directory {
public:
virtual ~Directory() = default;
virtual OS::OS::ResultAnd<uint32_t> GetEntries(FakeThread&, FakeFS&, uint32_t requested_entries, FileBuffer& out_entries) = 0;
};
class HostDirectory : public Directory {
std::filesystem::path path;
std::filesystem::directory_iterator iter;
public:
HostDirectory(const ValidatedHostPath& path) : path(path) {
if (!std::filesystem::exists(path.path)) {
// Required by Mario Kart 7 initial save file creation
// TODOTEST: Is this the right error code?
throw IPC::IPCError { 0, 0xc8804471 };
// throw std::runtime_error("Called OpenDirectory on nonexisting directory");
}
if (!std::filesystem::is_directory(path.path)) {
// TODOTEST: Is this the right error code?
throw IPC::IPCError { 0, 0xe0c04702 };
// throw std::runtime_error("Called OpenDirectory on path \"" + path.path.generic_string() + "\" that exists but is not a directory");
}
iter = std::filesystem::directory_iterator(path);
// TODOTEST: What happens if the directory is modified after opening and before enumerating its contents?
}
OS::OS::ResultAnd<uint32_t> GetEntries(FakeThread&, FakeFS&, uint32_t requested_entries, FileBuffer& out_entries) override {
// NOTE: This function is stateful: Subsequent calls skip previously retrieved entries
if (requested_entries == 0) {
// Return early to avoid advancing the directory iterator
// TODO: Should probably assert to avoid this entirely for now
return std::make_pair(RESULT_OK, uint32_t { 0 });
}
// auto it = iter;
uint32_t actual_entries = 0;
for (auto& entry : iter) {
// TODO: Merge with Entry defined by platform
struct Entry {
uint16_t name_utf16[0x20c / 2];
uint8_t short_name[0xa]; // short 8.3 filename without extension
uint8_t short_name_ext[0x4]; // short filename extension
uint8_t always_1;
uint8_t unknown;
uint8_t is_directory;
uint8_t is_hidden;
uint8_t is_archive;
uint8_t is_read_only;
uint64_t size;
} out_entry {};
static_assert(sizeof(out_entry) == 0x228);
out_entry.always_1 = 1;
out_entry.is_directory = entry.is_directory();
out_entry.is_read_only = false; // TODO
if (!entry.is_directory()) {
out_entry.size = entry.file_size();
}
ranges::copy(entry.path().filename().string(), out_entry.name_utf16);
// uint16_t
// for (char c : entry.path().filename()) {
// out_entry.name_utf8), entry.path().filename().c_str(), sizeof(out_entry.name_utf8));
// }
// strncpy(reinterpret_cast<char*>(out_entry.name_utf8), entry.path().filename().c_str(), sizeof(out_entry.name_utf8));
// TODO: Properly encode as 8.3?
strncpy(reinterpret_cast<char*>(out_entry.short_name), entry.path().filename().c_str(), sizeof(out_entry.short_name));
strncpy(reinterpret_cast<char*>(out_entry.short_name_ext), entry.path().extension().c_str(), sizeof(out_entry.short_name_ext));
// TODO: Short name + ext
// Some 3DS homebrew applications expect this field to be set for
// non-directory files. This does no harm as the bit would usually
// be set for an SD card used on the 3DS anyway.
out_entry.is_archive = !out_entry.is_directory;
// TODO: Use proper serialization
// TODOTEST: Does data after the null-terminated filename need to be zeroed out?
out_entries.Write(reinterpret_cast<char*>(&out_entry), sizeof(out_entry));
++actual_entries;
if (actual_entries >= requested_entries) {
break;
}
}
// TODO: If fewer than the total number of entries were requested, does the return value indicate how many entries there are in total?
return std::make_pair(RESULT_OK, actual_entries);
}
};
Result Archive::CreateFile(FakeThread& thread, FakeFS& context, uint32_t transaction, uint32_t attributes,
uint64_t initial_file_size, uint32_t file_path_type, IPC::StaticBuffer file_path) {
throw std::runtime_error(std::string("CreateFile not implemented for archive ") + typeid(*this).name());
}
ArchiveFormatInfo Archive::GetFormatInfo(FakeThread&, FakeFS&) {
throw std::runtime_error(std::string("GetFormatInfo not implemented for archive ") + typeid(*this).name());
}
class ArchivePXI : public Archive {
uint64_t handle;
uint32_t archive_id;
public:
ArchivePXI(FakeThread& thread, FSContext& context, uint32_t archive_id, uint32_t path_type, IPC::StaticBuffer path)
: handle(IPC::SendIPCRequest<Platform::PXI::FS::OpenArchive>(thread, context.pxifs_session, archive_id, path_type, path.size, path)),
archive_id(archive_id) {
}
std::pair<Result,std::unique_ptr<File>> OpenFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, uint32_t path_type, IPC::StaticBuffer path) override {
if (path_type != 2)
throw std::runtime_error(fmt::format("Unknown file path {:#x} for PXI archive id {:#x}", path_type, archive_id));
path = AdjustFilePath(thread, path);
auto file = IPC::SendIPCRequest<Platform::PXI::FS::OpenFile>(thread, context.pxifs_session, transaction, handle, path_type, path.size, open_flags, attributes, path);
auto file_ptr = std::unique_ptr<File>(new FilePXI(file));
return std::make_pair(RESULT_OK, std::move(file_ptr));
}
std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread&, FakeFS&, uint32_t path_type, IPC::StaticBuffer path) override {
throw std::runtime_error("OpenDirectory not implemented for ArchivePXI");
// return std::pair<Result,std::unique_ptr<Directory>>(RESULT_OK, nullptr);
}
Result CreateDirectory(FakeThread&, FakeFS&, uint32_t path_type, IPC::StaticBuffer path) override {
throw std::runtime_error("CreateDirectory not implemented for ArchivePXI");
}
virtual Result DeleteFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t path_type, IPC::StaticBuffer path) override final {
throw std::runtime_error(fmt::format("DeleteFile not implemented for ArchivePXI"));
}
virtual Result RenameFile(FakeThread&, FSContext&, uint32_t, uint32_t, IPC::StaticBuffer, uint32_t, IPC::StaticBuffer) override final {
throw std::runtime_error(fmt::format("RenameFile not implemented for ArchivePXI"));
}
virtual Result DeleteDirectory(FakeThread& thread, FSContext& context, uint32_t transaction, bool recursive, uint32_t path_type, IPC::StaticBuffer path) override final {
throw std::runtime_error(fmt::format("DeleteDirectory not implemented for ArchivePXI"));
}
// Can be customized by child classes to reorder or generate fields on-the-fly
virtual IPC::StaticBuffer AdjustFilePath(FakeThread& thread, IPC::StaticBuffer path) const {
return path;
}
};
class ArchiveDummy final : public Archive {
uint32_t archive_id;
public:
ArchiveDummy(FakeThread& thread, FSContext&, uint32_t path_type, IPC::StaticBuffer path, uint32_t archive_id) : archive_id(archive_id) {
thread.GetLogger()->info("path size {}", path.size);
// if (path_type != 1)
// throw std::runtime_error(fmt::format("Invalid path type (expected 1, got {})", path_type));
//
// if (path.size != 1)
// throw std::runtime_error(fmt::format("Invalid path size (expected 1, got {})", path.size));
// TODO: Implement. Stubbed for now to see what kind of files are opened for this
}
std::pair<Result,std::unique_ptr<File>> OpenFile(FakeThread&, FSContext&, uint32_t, uint32_t, uint32_t, uint32_t, IPC::StaticBuffer) override final {
throw std::runtime_error(fmt::format("TODO: Implement OpenFile for archive {:#x}", archive_id));
}
std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread&, FakeFS&, uint32_t path_type, IPC::StaticBuffer path) override {
throw std::runtime_error(fmt::format("OpenDirectory not implemented for {:#x}", archive_id));
}
Result CreateDirectory(FakeThread&, FakeFS&, uint32_t path_type, IPC::StaticBuffer path) override {
throw std::runtime_error(fmt::format("CreateDirectory not implemented for archive {:#x}", archive_id));
}
Result DeleteFile(FakeThread&, FSContext&, uint32_t, uint32_t, IPC::StaticBuffer) override final {
return RESULT_OK;
}
Result DeleteDirectory(FakeThread&, FSContext&, uint32_t, bool recursive, uint32_t, IPC::StaticBuffer) override final {
throw std::runtime_error(fmt::format("TODO: Implement OpenDirectory for archive {:#x}", archive_id));
}
Result RenameFile(FakeThread&, FakeFS&, uint32_t transaction, uint32_t source_path_type, IPC::StaticBuffer source_path, uint32_t target_path_type, IPC::StaticBuffer target_path) override final {
throw std::runtime_error(fmt::format("TODO: Implement RenameFile for archive {:#x}", archive_id));
}
};
/// Base class for archives that do not directly map to a PXIFS archive
class ArchiveNonPXI : public Archive, PathValidator {
public:
virtual ~ArchiveNonPXI() = default;
struct invalid_path_tag {};
struct empty_path_tag {};
using ValidatedPath = ValidatedHostPath;
private:
virtual Result ResultFileNotFound() const = 0; // 0xc8804470 for savegames, 0xc8804478 for sdmc
virtual Result ResultDirectoryNotFound() const = 0; // 0xc8804471 for savegames, 0xc8804478 for sdmc
virtual Result ResultFileAlreadyExists() const = 0; // 0xc82044b4 for savegames, 0xc82044be for sdmc
virtual Result ResultDirectoryAlreadyExists() const = 0; // 0xc82044b9 for savegames, 0xc82044be for sdmc
// Open from binary path
virtual std::pair<Result,std::unique_ptr<File>> OpenFile(FakeThread&, FSContext&, uint32_t, uint32_t, uint32_t, uint8_t[], size_t);
// Open from human readable UTF8 path
virtual std::pair<Result, std::unique_ptr<File>>
OpenFile(FakeThread&, FakeFS&, uint32_t transaction, uint32_t open_flags, uint32_t attributes, const ValidatedPath&);
virtual Result DeleteFile(FakeThread&, FakeFS&, uint32_t /*transaction*/, const ValidatedPath&) {
throw std::runtime_error(std::string("DeleteFile not implemented for archive ") + typeid(*this).name());
}
virtual Result RenameFile(FakeThread&, FakeFS&, uint32_t /*transaction*/, const ValidatedPath&, const ValidatedPath&) {
throw std::runtime_error(std::string("RenameFile not implemented for archive ") + typeid(*this).name());
}
virtual Result CreateDirectory(FakeThread&, FakeFS&, const ValidatedPath&) {
throw std::runtime_error(std::string("CreateDirectory not implemented for archive ") + typeid(*this).name());
}
virtual Result DeleteDirectory(FakeThread& thread, FSContext&, uint32_t /*transaction*/, bool /*recursive*/, const ValidatedPath&) {
throw std::runtime_error(std::string("DeleteDirectory not implemented for archive ") + typeid(*this).name());
}
virtual std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread&, FSContext&, const ValidatedPath&) {
throw std::runtime_error(std::string("OpenDirectory not implemented for archive ") + typeid(*this).name());
}
private:
std::pair<Result,std::unique_ptr<File>> OpenFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, uint32_t path_type, IPC::StaticBuffer path) override final {
switch (path_type) {
case 2:
{
// Binary path
std::vector<uint8_t> data(path.size);
auto DataAddressRange = ranges::view::ints(path.addr, path.addr + path.size);
auto ReadFromAddr = [&thread](auto addr) { return thread.ReadMemory(addr); };
ranges::transform(DataAddressRange, data.begin(), ReadFromAddr);
return OpenFile(thread, context, transaction, open_flags, attributes, data.data(), data.size());
}
default:
{
auto common_path = CommonPath(thread, path_type, path.size, path);
auto open_file_from_utf8 = [&](const Utf8PathType& utf8_path) {
return OpenFile(thread, context, transaction, open_flags, attributes,
ValidateAndGetSandboxedTreePath(utf8_path));
};
return common_path.Visit( open_file_from_utf8,
[](const BinaryPathType&) -> std::pair<Result,std::unique_ptr<File>> { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
}
}
protected:
virtual Result CreateFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t attributes,
uint64_t initial_file_size, const ValidatedPath& path) {
auto attempt_open_file = [&](uint32_t open_flags) {
return OpenFile(thread, context, transaction, open_flags, attributes, path);
};
Result result;
{
// Verify the file does not exist already
std::unique_ptr<File> file;
try {
std::tie(result, file) = attempt_open_file(1);
if (result == RESULT_OK) {
// File already exists
return test_mode ? ResultFileAlreadyExists()
: throw std::runtime_error("Tried to CreateFile with a path that points to an existing file");
}
} catch (IPC::IPCError& err) {
if (err.result != ResultFileNotFound()) {
// Re-throw unexpected error
throw;
}
}
// Actually create the file now
std::tie(result, file) = attempt_open_file(6 /* create and open writable */);
if (result != RESULT_OK) {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
std::tie(result) = file->SetSize(thread, context, initial_file_size);
if (result != RESULT_OK) {
throw std::runtime_error("Failed to set initial size of newly created file");
}
// Fill a dummy buffer with zeroes to initialize the file contents
auto buffer = thread.GetParentProcess().AllocateBuffer(64);
memset(buffer.second, 0, 64);
for (uint64_t offset = 0; offset < initial_file_size; offset += 64) {
auto bytes_to_write = static_cast<uint32_t>(std::min(uint64_t{64}, initial_file_size - offset));
uint32_t bytes_written;
std::tie(result, bytes_written) = file->Write(thread, context, offset, bytes_to_write, 0, buffer.first);
if (result != RESULT_OK || bytes_written != bytes_to_write) {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
}
thread.GetParentProcess().FreeBuffer(buffer.first);
}
return result;
}
private:
Result CreateFile(FakeThread& thread, FakeFS& context, uint32_t transaction, uint32_t attributes, uint64_t initial_file_size, uint32_t path_type, IPC::StaticBuffer path) override {
auto common_path = CommonPath(thread, path_type, path.size, path);
auto create_file_from_utf8 = [&](const Utf8PathType& utf8_path) {
return CreateFile( thread, context, transaction, attributes, initial_file_size,
ValidateAndGetSandboxedTreePath(utf8_path));
};
return common_path.Visit( create_file_from_utf8,
[](const BinaryPathType&) -> Result { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
Result DeleteFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t path_type, IPC::StaticBuffer path) override final {
auto delete_file_from_utf8 = [&](const Utf8PathType& utf8_path) {
return DeleteFile( thread, context, transaction,
ValidateAndGetSandboxedTreePath(utf8_path));
};
auto common_path = CommonPath(thread, path_type, path.size, path);
return common_path.Visit( delete_file_from_utf8,
[](const BinaryPathType&) -> Result { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
Result RenameFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t source_path_type, IPC::StaticBuffer source_path, uint32_t target_path_type, IPC::StaticBuffer target_path) override final {
auto error_unsupported_binary_path = [](const BinaryPathType&) -> Result { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); };
auto source_common_path = CommonPath(thread, source_path_type, source_path.size, source_path);
auto target_common_path = CommonPath(thread, target_path_type, target_path.size, target_path);
return source_common_path.Visit(
[&](const Utf8PathType& source_utf8_path) {
return target_common_path.Visit(
[&](const Utf8PathType& target_utf8_path) {
return RenameFile( thread, context, transaction,
ValidateAndGetSandboxedTreePath(source_utf8_path),
ValidateAndGetSandboxedTreePath(target_utf8_path));
}
, error_unsupported_binary_path);
},
error_unsupported_binary_path);
}
Result CreateDirectory(FakeThread& thread, FakeFS& context, uint32_t path_type, IPC::StaticBuffer path) override final {
auto create_dir_from_utf8 = [&](const Utf8PathType& utf8_path) {
return CreateDirectory( thread, context,
ValidateAndGetSandboxedTreePath(utf8_path));
};
auto common_path = CommonPath(thread, path_type, path.size, path);
return common_path.Visit( create_dir_from_utf8,
[](const BinaryPathType&) -> Result { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
Result DeleteDirectory(FakeThread& thread, FSContext& context, uint32_t transaction, bool recursive, uint32_t path_type, IPC::StaticBuffer path) override final {
auto delete_dir_from_utf8 = [&](const Utf8PathType& utf8_path) {
return DeleteDirectory( thread, context, transaction, recursive,
ValidateAndGetSandboxedTreePath(utf8_path));
};
auto common_path = CommonPath(thread, path_type, path.size, path);
return common_path.Visit( delete_dir_from_utf8,
[](const BinaryPathType&) -> Result { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread& thread, FakeFS& context, uint32_t path_type, IPC::StaticBuffer path) override {
auto open_dir_from_utf8 = [&](const Utf8PathType& utf8_path) {
return OpenDirectory( thread, context,
ValidateAndGetSandboxedTreePath(utf8_path));
};
auto common_path = CommonPath(thread, path_type, path.size, path);
return common_path.Visit( open_dir_from_utf8,
[](const BinaryPathType&) -> std::pair<Result,std::unique_ptr<Directory>> { throw std::runtime_error("Binary path handling for ArchiveNonPXI not implemented"); });
}
};
std::pair<Result,std::unique_ptr<File>> ArchiveNonPXI::OpenFile(FakeThread&, FSContext&, uint32_t, uint32_t, uint32_t, uint8_t*, size_t) {
throw std::runtime_error(std::string("OpenFile with binary path not implemented for archive ") + typeid(*this).name());
}
std::pair<Result, std::unique_ptr<File>>
ArchiveNonPXI::OpenFile(FakeThread&, FakeFS&, uint32_t, uint32_t, uint32_t, const ValidatedPath&) {
throw std::runtime_error(std::string("OpenFile not implemented for archive ") + typeid(*this).name());
}
/**
* Wrapper around ArchivePXI for NCCH access (archives 0x2345678a and 0x2345678e) to
* deal with differences between FSPXI and FS.
* When accessing the RomFS, FSPXI includes the full section whereas FS skips the
* first 0x1000 bytes (up to the level 3 header).
*/
class ArchiveNCCHSection : public ArchivePXI {
public:
ArchiveNCCHSection(FakeThread& thread, FSContext& context, uint32_t archive_id, uint32_t path_type, IPC::StaticBuffer path)
: ArchivePXI(thread, context, archive_id, path_type, path) {
ValidateContract(archive_id == 0x2345678a || archive_id == 0x2345678e);
}
std::pair<Result,std::unique_ptr<File>> OpenFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, uint32_t path_type, IPC::StaticBuffer path) override {
auto ret = ArchivePXI::OpenFile(thread, context, transaction, open_flags, attributes, path_type, path);
if (ret.first != RESULT_OK) {
return ret;
}
if (thread.ReadMemory32(path.addr + 8) != 0) {
// Non-RomFS sections are returned verbatimly
return ret;
}
auto size = ret.second->GetSize(thread, context);
if (std::get<0>(size) != RESULT_OK) {
throw Mikage::Exceptions::NotImplemented("Opened file but could not determine size");
}
auto sub_file = std::make_unique<SubFile>(std::shared_ptr<File> { ret.second.release() }, 0x1000, std::get<1>(size) - 0x1000);
return std::make_pair(RESULT_OK, std::move(sub_file));
}
};
/**
* Used to access sections (RomFS, ExeFS banner/icon/logo) of the
* client application's own NCCH
*/
class ArchiveOwnNCCHSection : public ArchiveNCCHSection {
public:
ArchiveOwnNCCHSection(FakeThread& thread, FSContext& context, uint32_t path_type, IPC::StaticBuffer path, ProgramInfo program_info)
: ArchiveNCCHSection(thread, context, 0x2345678a, path_type, AdjustArchivePath(thread, path, program_info)) {
}
static IPC::StaticBuffer AdjustArchivePath(FakeThread& thread, IPC::StaticBuffer path, ProgramInfo program_info) {
if (path.size != 1)
throw std::runtime_error("Expected archive path with 1 byte of data");
// TODO: Actually, archive 0x2345678a uses 16-byte archive paths!
path.size = 12;
thread.WriteMemory32(path.addr, program_info.program_id & 0xFFFFFFFF);
thread.WriteMemory32(path.addr + 4, program_info.program_id >> 32);
thread.WriteMemory32(path.addr + 8, program_info.media_type);
return path;
}
IPC::StaticBuffer AdjustFilePath(FakeThread& thread, IPC::StaticBuffer path) const override {
if (path.size != 12)
throw std::runtime_error("Expected file path with 12 bytes of data");
path.size = 20;
// Move input path 8 bytes ahead for PXIFS
thread.WriteMemory32(path.addr + 16, thread.ReadMemory32(path.addr + 8));
thread.WriteMemory32(path.addr + 12, thread.ReadMemory32(path.addr + 4));
thread.WriteMemory32(path.addr + 8, thread.ReadMemory32(path.addr + 0));
thread.WriteMemory32(path.addr, 0); // Request NCCH data
thread.WriteMemory32(path.addr + 4, 0); // Content index
return path;
}
};
/**
* Proxy for PXI archive 0x1234567c, but tied to one particular saveid (which
* is specified when opening the archive rather than when opening files).
*
* Files in this archive do not display the raw savegame; instead, the actual
* partition contents are accessible via the given file path.
*
* TODO: What mediatype argument is used when opening 0x1234567c? Is it just always 0, i.e. NAND?
* TODO: Is the first word of the path for this archive part of the saveid or actually the media type?
*/
class ArchiveSystemSaveDataForSaveId : public ArchiveNonPXI {
FSContext& context;
uint32_t mediatype;
uint64_t saveid;
void InitializeArchive(FakeThread& thread, FSContext& context) {
mediatype &= 0xff; // TODO: cfg savegame?
if (mediatype != 0) {
// Not supported, yet
thread.GetLogger()->warn("OpenArchive with mediatype {:#x} and saveid {:#x} failed due to unsupported mediatype",
mediatype, saveid);
throw std::runtime_error("Not implemented");
}
// TODO: This archive is supposed to be a proxy to a PXI archive,
// but we currently do not have any savegame parsing code
// to do this properly.
const bool accurate_proxy = false;
if (accurate_proxy) {
// Reuse static buffer but replace its data with the media type for the PXI archive
// const uint32_t media_type = 0;
// thread.WriteMemory32(path.addr, media_type);
// path.size = 4;
// handle = IPC::SendIPCRequest<Platform::PXI::FS::OpenArchive>(thread, context.pxifs_session, 0x1234567c, 0x2 /* BINARY */, path.size, path);
} else {
// Return error if the save file hasn't been created yet
if (!std::filesystem::exists(GetSandboxHostRoot())) {
throw IPC::IPCError{0, 0xc8804470};
}
}
}
std::filesystem::path GetSandboxHostRoot() const override {
return GetBaseFilePath(context, saveid);
}
// TODO: Verify
Result ResultFileNotFound() const override {
return 0xc8804470;
}
Result ResultDirectoryNotFound() const override {
return 0xc8804471;
}
Result ResultFileAlreadyExists() const override {
return 0xc82044b4;
}
Result ResultDirectoryAlreadyExists() const override {
return 0xc82044b9;
}
public:
ArchiveSystemSaveDataForSaveId(FakeThread& thread, FSContext& context, uint32_t path_type, IPC::StaticBuffer path) : context(context) {
if (path_type != 2)
throw std::runtime_error(fmt::format("Invalid path type (expected 2, got {})", path_type));
if (path.size != 8)
throw std::runtime_error(fmt::format("Invalid path size (expected 8, got {})", path.size));
// NOTE: The lower word of the saveid seems to be implied to be zero for this archive
mediatype = thread.ReadMemory32(path.addr);
saveid = thread.ReadMemory32(path.addr + 4);
InitializeArchive(thread, context);
}
ArchiveSystemSaveDataForSaveId(FakeThread& thread, FSContext& context, uint32_t mediatype, uint64_t saveid) : context(context), mediatype(mediatype), saveid(saveid) {
InitializeArchive(thread, context);
}
static std::string GetBaseFileParentPath(FSContext& context, uint64_t saveid) {
return fmt::format("./data/data/{:016x}/sysdata/{:08x}", GetId0(context), saveid & 0xFFFFFFFF);
}
static std::string GetBaseFilePath(FSContext& context, uint64_t saveid) {
return fmt::format("{}/{:08x}_extracted", GetBaseFileParentPath(context, saveid), saveid >> 32);
}
static void CreateSystemSaveData(FSContext& context, MediaType media_type, uint32_t saveid, uint32_t /*size*/) {
if (media_type != MediaType::NAND) {
throw Mikage::Exceptions::NotImplemented("Tried to CreateSystemSaveData for non-NAND mediatype");
}
if (std::filesystem::exists(GetBaseFilePath(context, saveid))) {
throw Mikage::Exceptions::NotImplemented("Tried to CreateSystemSaveData for save_id {:#x} that already existed", saveid);
}
// TODO: Adopt interface similar to ArchiveSaveDataBase instead
std::filesystem::create_directories(GetBaseFilePath(context, saveid));
}
std::pair<Result, std::unique_ptr<File>>
OpenFile(FakeThread& thread, FakeFS& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, const ValidatedPath& path) override {
const std::string savegame_path = GetBaseFilePath(context, saveid);
thread.GetLogger()->info("{}ArchiveSystemSaveDataForSaveId opening file \"{}{}\"",
ThreadPrinter{thread}, savegame_path, path.path);
try {
return { RESULT_OK, std::make_unique<FileSaveData>(context, path, ((open_flags & 0x4) != 0), true /* TODO: Implicit resize needed? */) };
} catch (std::ios_base::failure& exc) {
thread.GetLogger()->warn("Opening file failed: {}", exc.what());
// TODOTEST: What error code to return?
return { 0xc8804464, std::unique_ptr<FileSaveData>{} };
}
}
virtual Result DeleteFile(FakeThread& thread, FSContext&, uint32_t /*transaction*/, const ValidatedPath& path) override {
thread.GetLogger()->info("Deleting host file \"{}\"", path.path);
if (!std::filesystem::remove(path.path)) {
return ResultFileNotFound();
}
return RESULT_OK;
}
Result CreateDirectory(FakeThread&, FakeFS&, const ValidatedPath& validated_path) override {
auto path = validated_path.path;
// Drop redundant '/'s at the end, e.g "/edit/". std::filesystem turns
// them into "/edit/.", which then breaks create_directory since it
// expects all intermediate paths to exist (including /edit)
while (path == ".") {
path = path.parent_path();
}
if (std::filesystem::exists(path)) {
if (std::filesystem::is_directory(path)) {
// TODO: Requires testing!
return ResultDirectoryAlreadyExists();
}
throw std::runtime_error("Called CreateDirectory on a non-directory path that already exists");
}
std::filesystem::create_directory(path);
return RESULT_OK;
}
};
class ArchiveSaveDataProvider {
// Path to save data that is considered "created" but not "formatted"
std::string unformatted_path;
// Path to save data that is considered both "created" and "formatted"
std::string formatted_path;
// If true, writes past the end of file should implicitly resize the file
bool implicit_resize;
std::filesystem::path FormatInfoPath() const {
return std::filesystem::path { unformatted_path } / "metadata";
}
public:
ArchiveSaveDataProvider(std::string unformatted_path, std::string formatted_subpath, bool implicit_resize)
: unformatted_path(std::move(unformatted_path)), formatted_path(this->unformatted_path + "/" + std::move(formatted_subpath)),
implicit_resize(implicit_resize) {
}
bool IsCreated() const {
return std::filesystem::exists(unformatted_path);
}
bool IsFormatted() const {
return std::filesystem::exists(FormatInfoPath());
}
void Create() const {
if (IsCreated()) {
throw std::runtime_error(fmt::format("Create() called on save data file \"{}\" that already exists", unformatted_path));
}
EnsureCreated();
}
/// Idempotent version of Create: Does not return an error when the save data is already created
void EnsureCreated() const {
std::filesystem::create_directories(unformatted_path);
}
void Format(const ArchiveFormatInfo& format_info) const {
if (!IsCreated()) {
throw std::runtime_error(fmt::format("Format() called on save data file \"{}\" that does not exist", unformatted_path));
}
if (IsFormatted()) {
// TODO: Mario Kart 7 seems to hit this
// TODO: Super Street Fighter Demo seems to hit this
// throw std::runtime_error(fmt::format("Format() called on save data file \"{}\" that is already formatted", formatted_path));
}
EnsureFormatted(format_info);
}
void EnsureFormatted(const ArchiveFormatInfo& format_info) const {
std::filesystem::create_directories(formatted_path);
std::ofstream metadata(FormatInfoPath());
// TODO: Use serialization interface
metadata.write(reinterpret_cast<const char*>(&format_info), 3 * sizeof(uint32_t));
uint8_t duplicate_data = format_info.duplicate_data;
metadata.write(reinterpret_cast<char*>(&duplicate_data), sizeof(duplicate_data));
}
ArchiveFormatInfo ReadFormatInfo() const {
if (!IsFormatted()) {
throw std::runtime_error(fmt::format("ReadFormatInfo() called on save data file \"{}\" that has not been formatted", formatted_path));
}
std::ifstream metadata(FormatInfoPath());
// TODO: Use serialization interface
ArchiveFormatInfo format_info;
metadata.read(reinterpret_cast<char*>(&format_info), 3 * sizeof(uint32_t));
uint8_t duplicate_data;
metadata.read(reinterpret_cast<char*>(&duplicate_data), sizeof(duplicate_data));
format_info.duplicate_data = duplicate_data;
return format_info;
}
std::filesystem::path GetAbsolutePath(const Utf8PathType& raw_path) const {
return GetAbsolutePath(formatted_path, raw_path);
}
static std::filesystem::path GetAbsolutePath(const std::filesystem::path& base_path, const Utf8PathType& raw_path) {
if (raw_path.empty() || raw_path[0] != '/') {
throw std::runtime_error("Path must start with '/'");
}
std::filesystem::path path = base_path;
path /= raw_path.to_std_path();
path = path.lexically_relative(base_path);
if (path.begin() != path.end() && *path.begin() == "..") {
throw std::runtime_error("Path \"" + raw_path + "\" escapes sandbox root directory");
}
return std::filesystem::absolute(base_path / std::move(path));
}
std::string_view GetFormattedRootPath() const {
return formatted_path;
}
bool ShouldImplicitlyResize() const {
return implicit_resize;
}
};
class ArchiveSaveDataBase : public ArchiveNonPXI {
protected:
Result ResultFileNotFound() const override {
return 0xc8804470;
}
Result ResultDirectoryNotFound() const override {
return 0xc8804471;
}
Result ResultFileAlreadyExists() const override {
return 0xc82044b4;
}
Result ResultDirectoryAlreadyExists() const override {
return 0xc82044b9;
}
ArchiveSaveDataProvider provider;
ArchiveSaveDataBase(ArchiveSaveDataProvider provider_, Platform::FS::MediaType media_type)
: provider(std::move(provider_)) {
if (media_type == Platform::FS::MediaType::GameCard) {
// Card saves always exist as part of the physical gamecard chip
// TODO: The GameCard loader might be a better place to put this
provider.EnsureCreated();
}
if (!provider.IsCreated()) {
// Return "Not found"
throw IPC::IPCError { 0, 0xc8804464 };
}
if (!provider.IsFormatted()) {
// Archive has been created but not formatted: Return "Not formatted"
throw IPC::IPCError { 0, 0xc8a04554 };
}
}
ArchiveFormatInfo GetFormatInfo(FakeThread&, FakeFS&) override {
return provider.ReadFormatInfo();
}
std::filesystem::path GetSandboxHostRoot() const override {
return canonical(std::filesystem::path { std::string { provider.GetFormattedRootPath() } });
}
std::pair<Result, std::unique_ptr<File>>
OpenFile(FakeThread&, FakeFS& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, const ValidatedPath& path) override {
if (open_flags == 0) {
test_mode ? throw IPC::IPCError { 0, 0xe0c046f8 }
: throw std::runtime_error("Tried to OpenFile with invalid open flags");
}
return { RESULT_OK, std::make_unique<FileSaveData>(context, path, ((open_flags & 4) != 0), provider.ShouldImplicitlyResize()) };
}
Result DeleteFile(FakeThread& thread, FSContext&, uint32_t, const ValidatedPath& path) override {
if (!std::filesystem::exists(path)) {
// Non-asserting failure required e.g. by Super Mario 3D Land during startup
return ResultFileNotFound();
}
thread.GetLogger()->warn("Deleting file: {}", path.path);
std::filesystem::remove(path);
return RESULT_OK;
}
Result CreateDirectory(FakeThread&, FakeFS&, const ValidatedPath& validated_path) override {
auto path = validated_path.path;
// Drop redundant '/'s at the end, e.g "/edit/". std::filesystem turns
// them into "/edit/.", which then breaks create_directory since it
// expects all intermediate paths to exist (including /edit)
while (path == ".") {
path = path.parent_path();
}
if (std::filesystem::exists(path)) {
if (std::filesystem::is_directory(path)) {
// TODO: Requires testing!
return ResultDirectoryAlreadyExists();
}
throw std::runtime_error("Called CreateDirectory on a non-directory path that already exists");
}
std::filesystem::create_directory(path);
return RESULT_OK;
}
std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread&, FakeFS&, const ValidatedPath& path) override {
return { RESULT_OK, std::make_unique<HostDirectory>(path) };
}
Result DeleteDirectory(FakeThread& thread, FSContext&, uint32_t, bool recursive, const ValidatedPath& path) override {
thread.GetLogger()->warn("Deleting directory{}: {}", recursive ? " recursively" : "", path.path);
if (!std::filesystem::exists(path.path)) {
thread.GetLogger()->warn("Directory {} does not exist", path.path);
return test_mode ? ResultDirectoryNotFound()
: throw std::runtime_error("Called DeleteDirectory on nonexisting directory");
}
if (!std::filesystem::is_directory(path.path)) {
throw std::runtime_error("Called DeleteDirectory on something that's not a directory");
}
if (recursive) {
throw std::runtime_error("Recursive deletion not supported yet");
}
[[maybe_unused]] bool result = std::filesystem::remove(path.path);
assert(result); // "remove" should only return false if the directory didn't exist, which we checked for above
return RESULT_OK;
}
};
// TODO: This archive should be deferred to PXI archive 0x2345678a instead
class ArchiveSaveData : public ArchiveSaveDataBase {
public:
ArchiveSaveData(FSContext& context, ProgramInfo& program_info) :
ArchiveSaveDataBase(GetProvider(context, program_info), Platform::FS::MediaType { program_info.media_type }) {
}
static ArchiveSaveDataProvider GetProvider(FSContext& context, ProgramInfo& program_info) {
const bool implicit_resize = true;
return ArchiveSaveDataProvider(BuildBasePath(context, program_info), "00000001.sav.extracted", implicit_resize);
}
static std::string BuildBasePath(FSContext& context, const ProgramInfo& program_info) {
// TODO: Check if save data is formatted, if not return error
if (program_info.media_type == 0 /* NAND */) {
// TODO: Figure out if NAND titles actually support this archive. Chances are NAND titles must always use SystemSaveData instead!
throw std::runtime_error("NAND save data files are not supported, yet");
}
// TODO: For now, we automatically map files within save data to files on the host file system.
// For compatibility with FSPXI, we should instead map each title's entire save data to one host file.
uint32_t program_id_high = (program_info.program_id >> 32);
uint32_t program_id_low = (program_info.program_id & 0xFFFFFFFF);
if (program_info.media_type == 1 /* SD */) {
return fmt::format("data/sdmc/Nintendo 3DS/{:016x}/{:016x}/title/{:08x}/{:08x}/data", GetId0(context), GetId1(context), program_id_high, program_id_low);
} else if (program_info.media_type == 2 /* Gamecard */) {
return fmt::format("data/card_savedata/{:08x}/{:08x}/data", program_id_high, program_id_low);
} else {
throw std::runtime_error("Unknown media type");
}
}
};
/**
* "Extra Data", which is stored on the SD card even for NAND titles. An
* exception to this is "shared extdata", which is stored on NAND. This archive
* is mostly a simplified version ArchiveSaveData (file sizes cannot be changed
* after creation, and there are no dedicated read-only or write-only file
* opening modes).
*/
class ArchiveExtSaveData : public ArchiveSaveDataBase {
public:
ArchiveExtSaveData(FakeThread& thread, FSContext& context, const ExtSaveDataInfo& info, bool is_shared, bool is_spotpass) :
ArchiveSaveDataBase(GetProvider(context, info, is_shared, is_spotpass), info.media_type) {
}
static ArchiveSaveDataProvider GetProvider(FSContext& context, const ExtSaveDataInfo& info, bool is_shared, bool is_spotpass) {
if (is_spotpass) {
// Uses /boss subdirectory rather than /user
throw std::runtime_error("SpotPass extdata not supported, yet");
}
const bool implicit_resize = false;
return ArchiveSaveDataProvider(BuildBasePath(context, info, is_shared, is_spotpass), "user", implicit_resize);
}
// For FSUser::CreateExtSaveData
static Result CreateSaveData(FakeThread&, FSContext& context, const ExtSaveDataInfo& info, uint32_t max_directories, uint32_t max_files, bool is_shared) {
// Note FSUser::CreateExtSaveData is idempotent: Calling it on an already created save data file is not an error
auto provider = GetProvider(context, info, is_shared, false);
provider.EnsureCreated();
// ExtSaveData is formatted automatically
const uint32_t size = 0; // TODO: Which size should be reported? Citra seems to use 0, but FSUser::CreateExtSaveData does take a size limit parameter, too
const bool duplicate_data = false; // TODO: Verify this is the returned value
provider.EnsureFormatted(ArchiveFormatInfo { size, max_directories, max_files, duplicate_data });
return RESULT_OK;
}
// For FSUser::DeleteExtSaveData
// TODO: Move to ArchiveSaveDataProvider
static Result DeleteSaveData(FakeThread& thread, FSContext& context, const ExtSaveDataInfo& info, bool is_shared) {
std::filesystem::path path = BuildBasePath(context, info, is_shared, false);
if (!std::filesystem::exists(path)) {
return test_mode ? 0xc8804478 : throw std::runtime_error("Tried to delete extdata that doesn't exist");
}
if (!std::filesystem::is_directory(path)) {
throw std::runtime_error("Tried to remove extdata file that somehow is not a directory");
}
thread.GetLogger()->warn("Deleting extdata directory {}", path);
std::filesystem::remove_all(path);
return RESULT_OK;
}
private:
static std::string BuildBasePath(FSContext& context, ExtSaveDataInfo info, bool is_shared, bool is_spotpass) {
if (is_shared && (info.media_type != Platform::FS::MediaType::NAND && info.media_type != Platform::FS::MediaType::SD)) {
throw std::runtime_error("Shared extdata must be stored on NAND or SD");
}
if (!is_shared && info.media_type != Platform::FS::MediaType::SD) {
throw std::runtime_error("Non-shared extdata must be stored on SD");
}
// TODO: For now, we automatically map files within save data to files on the host file system.
// For compatibility with FSPXI, we should instead map each title's entire save data to one host file.
return std::invoke([&]() -> std::string {
// NAND always uses 00048000 here, whereas SD always uses 0
// NOTE: As per Citra PR 3242, the FS module overrides the high extdata id with this value for shared extdata files on NAND
// TODO: Check if it should do so for all operations! (I think for either OpenArchive or CreateExtSaveData we shouldn't?)
if (is_shared && info.media_type == Platform::FS::MediaType::NAND) {
info.save_id = (info.save_id & 0xffff'ffff) | 0x48000'00000000;
}
uint32_t extdata_id_low = info.save_id & 0xffffffff;
uint32_t extdata_id_high = info.save_id >> 32;
if (info.media_type == Platform::FS::MediaType::NAND) {
return fmt::format("data/data/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), extdata_id_high, extdata_id_low);
} else {
return (HostSdmcDirectory() / fmt::format("Nintendo 3DS/{:016x}/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), GetId1(context), extdata_id_high, extdata_id_low)).string();
}
});
}
Result CreateFile(FakeThread& thread, FSContext& context, uint32_t transaction, uint32_t attributes,
uint64_t initial_file_size, const ValidatedPath& path) override {
// TODO: Use GetAbsolutePath!!
if (initial_file_size == 0) {
return test_mode ? 0xe0c046f8
: throw std::runtime_error("Tried to CreateFile an ExtData file with zero size");
}
auto attempt_open_file = [&](uint32_t open_flags) {
// Using Base's OpenFile here since our OpenFile doesn't allow file creation
return ArchiveSaveDataBase::OpenFile(thread, context, transaction, open_flags, attributes, path);
};
Result result;
{
// Verify the file does not exist already
std::unique_ptr<File> file;
try {
std::tie(result, file) = attempt_open_file(1);
if (result == RESULT_OK) {
// File already exists (non-asserting failure required by
// our internal creation logic for shared extra data in
// archive 0x00048000f000000b)
return ResultFileAlreadyExists();
}
} catch (IPC::IPCError& err) {
if (err.result != ResultFileNotFound()) {
// Re-throw unexpected error
throw;
}
}
// Actually create the file now
std::tie(result, file) = attempt_open_file(6 /* create and open writable */);
if (result != RESULT_OK) {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
uint64_t s;
std::tie(result, s) = file->GetSize(thread, context);
std::tie(result) = file->SetSize(thread, context, initial_file_size);
if (result != RESULT_OK) {
throw std::runtime_error("Failed to set initial size of newly created file");
}
// Fill a dummy buffer with zeroes to initialize the file contents
auto buffer = thread.GetParentProcess().AllocateBuffer(64);
memset(buffer.second, 0, 64);
for (uint64_t offset = 0; offset < initial_file_size; offset += 64) {
auto bytes_to_write = static_cast<uint32_t>(std::min(uint64_t{64}, initial_file_size - offset));
uint32_t bytes_written;
std::tie(result, bytes_written) = file->Write(thread, context, offset, bytes_to_write, 0, buffer.first);
if (result != RESULT_OK || bytes_written != bytes_to_write) {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
}
thread.GetParentProcess().FreeBuffer(buffer.first);
}
return result;
}
std::pair<Result, std::unique_ptr<File>>
OpenFile(FakeThread& thread, FakeFS& context, uint32_t transaction, uint32_t open_flags, uint32_t attributes, const ValidatedPath& path) override {
// Prohibit implicit file creation
if (open_flags & 0b100) {
test_mode ? throw IPC::IPCError {0, 0xe0c046f8 }
: throw std::runtime_error("Extdata does not support creating files via OpenFile");
}
// If the open_flags are valid at all (either readable or writeable), the extdata archive automatically opens as read-write
if (open_flags & 0b11) {
open_flags |= 0b11;
}
try {
return ArchiveSaveDataBase::OpenFile(thread, context, transaction, open_flags, attributes, path);
} catch (IPC::IPCError& err) {
return std::pair<Result, std::unique_ptr<File>>(err.result, nullptr);
}
}
};
class ArchiveHostDir : public ArchiveNonPXI {
std::filesystem::path base_path;
Result ResultFileNotFound() const override {
return GenericHostFile::result_file_not_found;
}
Result ResultDirectoryNotFound() const override {
// Same as for files
return GenericHostFile::result_directory_not_found;
}
Result ResultFileAlreadyExists() const override {
return GenericHostFile::result_file_already_exists;
}
Result ResultDirectoryAlreadyExists() const override {
return GenericHostFile::result_directory_already_exists;
}
public:
ArchiveHostDir(std::filesystem::path base_path) : base_path(std::move(base_path)) {
}
std::filesystem::path GetSandboxHostRoot() const override {
return std::filesystem::canonical(base_path);
}
std::pair<Result, std::unique_ptr<File>>
OpenFile(FakeThread&, FakeFS&, uint32_t /*transaction*/, uint32_t open_flags, uint32_t /*attributes*/, const ValidatedPath& path) override {
bool create = ((open_flags & 4) != 0);
return { RESULT_OK, std::make_unique<GenericHostFile>(path, create) };
}
Result CreateDirectory(FakeThread&, FSContext&, const ValidatedPath& path) override {
if (std::filesystem::exists(path)) {
// NOTE: Steel Diver hits this case during startup for the SDMC archive
return ResultDirectoryAlreadyExists();
}
std::filesystem::create_directory(path);
return RESULT_OK;
}
Result DeleteFile(FakeThread& thread, FSContext&, uint32_t /*transaction*/, const ValidatedPath& path) override {
thread.GetLogger()->warn("Deleting file: {}", path.path);
if (!std::filesystem::exists(path)) {
thread.GetLogger()->warn("File {} does not exist", path.path);
return test_mode ? ResultFileNotFound()
: throw std::runtime_error("Called DeleteFile on nonexisting file");
}
if (!std::filesystem::is_regular_file(path)) {
throw std::runtime_error("Called DeleteFile on something that's not a file");
}
[[maybe_unused]] bool result = std::filesystem::remove(path);
assert(result); // "remove" should only return false if the directory didn't exist, which we checked for above
return RESULT_OK;
}
Result RenameFile(FakeThread&, FakeFS&, uint32_t /*transaction*/, const ValidatedPath& source_path, const ValidatedPath& target_path) override {
if (!std::filesystem::exists(source_path)) {
throw Mikage::Exceptions::NotImplemented("Called RenameFile on a nonexisting source file");
}
if (!std::filesystem::is_regular_file(source_path)) {
throw std::runtime_error("Called RenameFile on something that's not a file");
}
if (std::filesystem::exists(target_path)) {
throw Mikage::Exceptions::NotImplemented("Called RenameFile with a target path that already exists");
}
std::filesystem::rename(source_path, target_path);
return RESULT_OK;
}
Result DeleteDirectory(FakeThread& thread, FSContext&, uint32_t /*transaction*/, bool recursive, const ValidatedPath& path) override {
thread.GetLogger()->warn("Deleting directory{}: {}", recursive ? " recursively" : "", path.path);
if (!std::filesystem::exists(path)) {
thread.GetLogger()->warn("Directory {} does not exist", path.path);
// NOTE: Steel Diver hits this case during startup if the "sdmc:/Nintendo 3DS" folder does not exist
return ResultDirectoryNotFound();
}
if (!std::filesystem::is_directory(path)) {
throw std::runtime_error("Called DeleteDirectory on something that's not a directory");
}
if (recursive) {
throw std::runtime_error("Recursive deletion not supported yet");
}
try {
[[maybe_unused]] bool result = std::filesystem::remove(path);
assert(result); // "remove" should only return false if the directory didn't exist, which we checked for above
} catch (...) {
// NOTE: Steel Diver hits this case during startup for the SDMC archive
return 0xc92044fa;
}
return RESULT_OK;
}
std::pair<Result,std::unique_ptr<Directory>> OpenDirectory(FakeThread&, FSContext&, const ValidatedPath& path) override {
return { RESULT_OK, std::make_unique<HostDirectory>(path) };
}
};
static std::string IosExceptionInfo(std::ios_base::failure& err) {
// On most systems (gcc included), err.code().message() doesn't return a
// helpful message. On the other hand, reading errno isn't guaranteed to
// work. So let's just print both, hoping that one of them will give us
// some useful information...
return fmt::format("{} / {}", err.code().message(), strerror(errno));
}
static OS::OS::ResultAnd<Handle>
OnIPCFileOpenSubFile( FakeThread& thread, FSContext& context,
std::shared_ptr<File> file, uint64_t file_offset, uint64_t num_bytes) {
auto sub_file = std::make_unique<SubFile>(file, file_offset, num_bytes);
// Create file session object and append it to the file session handler thread
OS::Result result;
HandleTable::Entry<ServerSession> server_session;
HandleTable::Entry<ClientSession> client_session;
std::tie(result,server_session,client_session) = thread.CallSVC(&OS::OS::SVCCreateSession);
if (result != RESULT_OK) {
throw Mikage::Exceptions::NotImplemented("Failed to create sub file session");
}
server_session.second->name = "SubFile_ServerSession";
client_session.second->name = "SubFile_ClientSession";
context.service.Append(server_session, std::move(sub_file));
return std::make_tuple(RESULT_OK, client_session.first);
}
static OS::OS::ResultAnd<uint32_t, IPC::MappedBuffer>
OnIPCFileRead( FakeThread& thread, FSContext& context,
File& file, uint64_t file_offset,
uint32_t num_bytes, IPC::MappedBuffer buffer) {
OS::Result result;
uint32_t bytes_read;
auto file_buffer = FileBufferInEmulatedMemory { thread, buffer.addr };
std::tie(result, bytes_read) = file.Read(context.file_context, context, file_offset, num_bytes, file_buffer);
return std::make_tuple(RESULT_OK, bytes_read, buffer);
}
static OS::OS::ResultAnd<uint32_t, IPC::MappedBuffer>
OnIPCFileWrite( FakeThread& thread, FSContext& context,
File& file, uint64_t file_offset,
uint32_t num_bytes, uint32_t options,
IPC::MappedBuffer buffer) {
// TODO: What do the "options" indicate? Observed values: 0x10001
// TODO: Does this operation automatically update the file size?
OS::Result result;
uint32_t bytes_written;
std::tie(result, bytes_written) = file.Write(thread, context, file_offset, num_bytes, options, buffer.addr);
return std::make_tuple(RESULT_OK, bytes_written, buffer);
}
static decltype(HLE::OS::ServiceHelper::SendReply) OnFileIPCRequest(FakeThread& thread, FSContext& context, std::shared_ptr<File> file, int32_t session_index, const IPC::CommandHeader& header) try {
using namespace Platform::FS::File;
if (file->HasOpenSubFile() && header.command_id != Close::id) {
throw Mikage::Exceptions::Invalid("Attempted to use file with open sub file");
}
switch (header.command_id) {
case OpenSubFile::id:
IPC::HandleIPCCommand<OpenSubFile>(OnIPCFileOpenSubFile, thread, thread, context, file);
break;
case Read::id:
IPC::HandleIPCCommand<Read>(OnIPCFileRead, thread, thread, context, *file);
break;
case Write::id:
IPC::HandleIPCCommand<Write>(OnIPCFileWrite, thread, thread, context, *file);
break;
case GetSize::id:
{
OS::Result result;
uint64_t size;
std::tie(result, size) = file->GetSize(thread, context);
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 3, 0).raw);
thread.WriteTLS(0x84, result);
thread.WriteTLS(0x88, size & 0xFFFFFFFF);
thread.WriteTLS(0x8c, size >> 32);
break;
}
case SetSize::id:
{
OS::Result result;
uint64_t size = (static_cast<uint64_t>(thread.ReadTLS(0x88)) << 32) | thread.ReadTLS(0x84);
std::tie(result) = file->SetSize(thread, context, size);
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, result);
break;
}
case 0x80a: // SetPriority
{
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
case OpenLinkFile::id:
{
OS::Result result;
HandleTable::Entry<ServerSession> server_session;
HandleTable::Entry<ClientSession> client_session;
std::tie(result,server_session,client_session) = thread.CallSVC(&OS::OS::SVCCreateSession);
if (result != RESULT_OK) {
throw std::runtime_error("Failed to create session for link file");
}
context.service.Append(server_session, std::get<std::shared_ptr<File>>(context.service.handle_data[session_index]));
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 2).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x88, IPC::TranslationDescriptor::MakeHandles(1, true).raw);
thread.WriteTLS(0x8c, client_session.first.value);
break;
}
case Close::id:
{
// NOTE: Close() will keep the file session open; it just makes all
// file operations return an error. This is in line with the
// official implementation.
context.service.ReplaceFileServer(session_index, std::make_unique<FileClosed>());
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
case Flush::id:
{
// Nothing to do
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
default:
throw Mikage::Exceptions::NotImplemented("Unknown fs file IPC command with header {:#010x}", header.raw);
}
return HLE::OS::ServiceHelper::SendReply;
} catch (std::ios_base::failure& err) {
thread.GetLogger()->error("Unexpected fstream exception in File IPC command {:#x} handled via object {}: {}",
header.command_id, boost::core::demangle(typeid(*file).name()),
IosExceptionInfo(err));
throw;
}
static OS::OS::ResultAnd<uint32_t, IPC::MappedBuffer>
OnIPCDirRead( FakeThread& thread, FSContext& context,
Directory& dir, uint32_t requested_entries,
IPC::MappedBuffer buffer) {
auto file_buffer = FileBufferInEmulatedMemory { thread, buffer.addr };
auto [result, actual_entries] = dir.GetEntries(thread, context, requested_entries, file_buffer);
return std::make_tuple(RESULT_OK, actual_entries, buffer);
}
static decltype(HLE::OS::ServiceHelper::SendReply) OnDirIPCRequest(FakeThread& thread, FSContext& context, Directory& dir, int32_t session_index, const IPC::CommandHeader& header) try {
using namespace Platform::FS::Dir;
switch (header.command_id) {
case Read::id:
IPC::HandleIPCCommand<Read>(OnIPCDirRead, thread, thread, context, dir);
break;
case Close::id:
{
// TODOTEST: Is this command similar to File::Close, in that it keeps
// the session itself open? That's what our implementation
// does for now
// TODO: REENABLE
// context.service.ReplaceFileServer(session_index, std::make_unique<DirClosed>());
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
default:
throw Mikage::Exceptions::NotImplemented("Unknown fs directory IPC command with header {:#010x}", header.raw);
}
return HLE::OS::ServiceHelper::SendReply;
} catch (std::ios_base::failure& err) {
thread.GetLogger()->error("Unexpected fstream exception in Directory IPC command {:#x} handled via object {}: {}",
header.command_id, boost::core::demangle(typeid(dir).name()),
IosExceptionInfo(err));
throw;
}
template<typename Class, typename Func>
static auto BindMemFn(Func f, Class* c) {
return [f,c](auto&&... args) { return std::mem_fn(f)(c, args...); };
}
FakeFS::FakeFS(FakeThread& thread)
: process(thread.GetParentProcess()),
logger(*thread.GetLogger()) {
OS::Result result;
// Get PxiFS0 service handle via srv:
HandleTable::Entry<ClientSession> srv_session;
std::tie(result,srv_session) = thread.CallSVC(&OS::OS::SVCConnectToPort, "srv:");
if (result != RESULT_OK)
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
IPC::SendIPCRequest<Platform::SM::SRV::RegisterClient>(thread, srv_session.first, IPC::EmptyValue{});
pxifs_session = IPC::SendIPCRequest<Platform::SM::SRV::GetServiceHandle>(thread, srv_session.first,
Platform::SM::PortName("PxiFS0"), 0);
thread.CallSVC(&OS::OS::SVCCloseHandle, srv_session.first);
auto fsreg_thread = std::make_shared<WrappedFakeThread>(thread.GetParentProcess(), [this](FakeThread& thread) { return FSRegThread(thread); });
fsreg_thread->name = "fs:REGThread";
process.AttachThread(fsreg_thread);
fsuser_handle_index = service.Append(OS::ServiceUtil::SetupService(thread, "fs:USER", 30));
fsldr_handle_index = service.Append(OS::ServiceUtil::SetupService(thread, "fs:LDR", 2));
FSUserThread(thread);
}
FakeFS::~FakeFS() = default;
template<typename T>
uint32_t FSServiceHelper::Append(HandleTable::Entry<T> entry, HandleData data) {
auto ret = HLE::OS::ServiceHelper::Append(entry);
handle_data.emplace_back(std::move(data));
return ret;
}
void FSServiceHelper::Erase(int32_t index) {
HLE::OS::ServiceHelper::Erase(index);
handle_data.erase(handle_data.begin() + index);
}
void FSServiceHelper::OnNewSession(int32_t port_index, HandleTable::Entry<ServerSession> session) {
ServiceHelper::OnNewSession(port_index, session);
handle_data.emplace_back(static_cast<uint32_t>(port_index));
}
void FSServiceHelper::ReplaceFileServer(int32_t index, std::unique_ptr<File> new_file) {
auto& data = handle_data[index];
if (!std::holds_alternative<std::shared_ptr<File>>(data)) {
throw std::runtime_error("Attempted to replace File object in non-File session");
}
data = std::move(new_file);
}
// Actually fs:USER *and* fs:LDR thread
void FakeFS::FSUserThread(FakeThread& thread) {
// TODO: These must be backed by emulated physical memory, since otherwise PXI can't actually read from them
auto path_buffer_addr = thread.GetParentProcess().AllocateStaticBuffer(0x30);
auto archive_path_buffer_addr = thread.GetParentProcess().AllocateStaticBuffer(0x30);
// Allocate static buffers and set up TLS descriptors for them
// NOTE: Some IPC commands (e.g. OpenFileDirectly) will forward the data
// in these buffers to the PXI process as a PXI buffer (i.e. as a
// table of physical memory chunks). Hence, they must be backed by
// physical memory rather than fake memory.
Result result;
std::tie(result, static_buffer_addr[0]) = thread.CallSVC(&OS::OS::SVCControlMemory, 0, 0, static_buffer_size * 3, 3 /* COMMIT */, 3 /* RW */);
thread.WriteTLS(0x180, IPC::TranslationDescriptor::MakeStaticBuffer(0, static_buffer_size).raw);
thread.WriteTLS(0x184, static_buffer_addr[0]);
static_buffer_addr[1] = static_buffer_addr[0] + static_buffer_size;
thread.WriteTLS(0x188, IPC::TranslationDescriptor::MakeStaticBuffer(0, static_buffer_size).raw);
thread.WriteTLS(0x18c, static_buffer_addr[1]);
static_buffer_addr[2] = static_buffer_addr[1] + static_buffer_size;
thread.WriteTLS(0x190, IPC::TranslationDescriptor::MakeStaticBuffer(0, static_buffer_size).raw);
thread.WriteTLS(0x194, static_buffer_addr[2]);
auto InvokeCommandHandler = [&](FakeThread& thread, uint32_t signalled_handle_index) {
Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) };
auto signalled_handle = service.handles.at(signalled_handle_index);
auto& data = service.handle_data.at(signalled_handle_index);
if (std::holds_alternative<uint32_t>(data)) {
return UserCommandHandler(thread, signalled_handle, header);
} else if (auto* file = std::get_if<std::shared_ptr<File>>(&data)) {
return OnFileIPCRequest(thread, *this, *file, signalled_handle_index, header);
} else if (auto* dir = std::get_if<std::shared_ptr<Directory>>(&data)) {
return OnDirIPCRequest(thread, *this, **dir, signalled_handle_index, header);
} else {
return HLE::OS::ServiceHelper::DoNothing;
}
};
service.Run(thread, std::move(InvokeCommandHandler));
}
static std::tuple<OS::Result, uint32_t> HandleIsSdmcWritable(FSContext& context, FakeThread& thread) {
thread.GetLogger()->info("{}received IsSdmcWritable", ThreadPrinter{thread});
return std::make_tuple(RESULT_OK, true /*false*/);
}
decltype(HLE::OS::ServiceHelper::SendReply) FakeFS::UserCommandHandler(FakeThread& thread, Handle sender, const IPC::CommandHeader& header) try {
using namespace Platform::FS::User;
// TODO: The registered handle should be removed from fsuser_process_ids when the session is closed!
if (header.command_id == Initialize::id) {
IPC::HandleIPCCommand<Initialize>(BindMemFn(&FakeFS::HandleInitialize, this), thread, thread, sender);
return HLE::OS::ServiceHelper::SendReply;
} else if (header.command_id == InitializeWithSdkVersion::id) {
IPC::HandleIPCCommand<InitializeWithSdkVersion>(BindMemFn(&FakeFS::HandleInitializeWithSdkVersion, this), thread, thread, sender);
return HLE::OS::ServiceHelper::SendReply;
}
// If this is not an Initialize command, we need to lookup the session context first.
// TODO: What if the client calls svcDuplicateHandle on the "sender" handle
// and then tries to use this service? With our current
// implementation, this would fail, but does it work on the actual
// system?
if (fsuser_process_ids.count(sender) == 0) {
logger.error("Tried to use FS:User without calling Initialize on {}", HandlePrinter{thread,sender});
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
auto session_pid = fsuser_process_ids[sender];
switch (header.command_id) {
case OpenFile::id:
IPC::HandleIPCCommand<OpenFile>(BindMemFn(&FakeFS::HandleOpenFile, this), thread, thread, session_pid);
break;
case OpenFileDirectly::id:
IPC::HandleIPCCommand<OpenFileDirectly>(BindMemFn(&FakeFS::HandleOpenFileDirectly, this), thread, thread, session_pid);
break;
case DeleteFile::id:
IPC::HandleIPCCommand<DeleteFile>(BindMemFn(&FakeFS::HandleDeleteFile, this), thread, thread, session_pid);
break;
case RenameFile::id:
IPC::HandleIPCCommand<RenameFile>(BindMemFn(&FakeFS::HandleRenameFile, this), thread, thread);
break;
case DeleteDirectory::id:
IPC::HandleIPCCommand<DeleteDirectory>(std::mem_fn(&FakeFS::HandleDeleteDirectory), thread, this, thread, session_pid, false);
break;
case DeleteDirectoryRecursively::id:
IPC::HandleIPCCommand<DeleteDirectoryRecursively>(std::mem_fn(&FakeFS::HandleDeleteDirectory), thread, this, thread, session_pid, true);
break;
case CreateFile::id:
IPC::HandleIPCCommand<CreateFile>(BindMemFn(&FakeFS::HandleCreateFile, this), thread, thread, session_pid);
break;
case CreateDirectory::id:
IPC::HandleIPCCommand<CreateDirectory>(BindMemFn(&FakeFS::HandleCreateDirectory, this), thread, thread, session_pid);
break;
case OpenDirectory::id:
IPC::HandleIPCCommand<OpenDirectory>(BindMemFn(&FakeFS::HandleOpenDirectory, this), thread, thread, session_pid);
break;
case OpenArchive::id:
IPC::HandleIPCCommand<OpenArchive>(BindMemFn(&FakeFS::HandleOpenArchive, this), thread, thread, session_pid);
break;
case ControlArchive::id:
IPC::HandleIPCCommand<ControlArchive>(BindMemFn(&FakeFS::HandleControlArchive, this), thread, thread, session_pid);
break;
case CloseArchive::id:
IPC::HandleIPCCommand<CloseArchive>(BindMemFn(&FakeFS::HandleCloseArchive, this), thread, thread, session_pid);
break;
case FormatOwnSaveData::id:
IPC::HandleIPCCommand<FormatOwnSaveData>(BindMemFn(&FakeFS::HandleFormatOwnSaveData, this), thread, thread, session_pid);
break;
case CreateSystemSaveDataLegacy::id:
{
auto implied_mediatype = MediaType::NAND;
IPC::HandleIPCCommand<CreateSystemSaveDataLegacy>(BindMemFn(&FakeFS::HandleCreateSystemSaveData, this), thread, thread, session_pid, static_cast<uint32_t>(implied_mediatype));
break;
}
case 0x812: // GetFreeBytes (Super Smash Bros)
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x88, 0x10000000); // Lots of free bytes
break;
case IsSdmcDetected::id:
IPC::HandleIPCCommand<IsSdmcDetected>(BindMemFn(&FakeFS::HandleIsSdmcDetected, this), thread, thread);
break;
case IsSdmcWritable::id:
IPC::HandleIPCCommand<IsSdmcWritable>(HandleIsSdmcWritable, thread, *this, thread);
break;
case 0x821: // CardSlotIsInserted
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x88, (thread.GetOS().setup.gamecard != nullptr)); // True if game card inserted
break;
case CreateExtSaveDataLegacy::id:
{
IPC::HandleIPCCommand<CreateExtSaveDataLegacy>(BindMemFn(&FakeFS::HandleCreateExtSaveDataLegacy, this), thread, thread, session_pid);
break;
}
case DeleteExtSaveDataLegacy::id:
{
// Forward to DeleteExtSaveData
auto media_type = MediaType { static_cast<uint8_t>(thread.ReadTLS(0x84)) };
uint64_t save_id = thread.ReadTLS(0x88); // Upper 32-bits are implied to be zero
auto [result] = HandleDeleteExtSaveData(thread, session_pid, ExtSaveDataInfo { media_type, {}, save_id, {} });
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, result);
break;
}
case 0x83a: // GetSpecialContentIndex
{
auto media_type = MediaType { static_cast<uint8_t>(thread.ReadTLS(0x84)) };
auto content_type = thread.ReadTLS(0x90) & 0xff;
if (/*media_type == MediaType::GameCard*/ true) {
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
if (content_type == 1) {
thread.WriteTLS(0x88, Meta::to_underlying(Loader::NCSDPartitionId::UpdateData));
} else if (content_type == 2) {
thread.WriteTLS(0x88, Meta::to_underlying(Loader::NCSDPartitionId::Manual));
} else if (content_type == 3) {
thread.WriteTLS(0x88, Meta::to_underlying(Loader::NCSDPartitionId::DownloadPlayChild));
} else {
throw Mikage::Exceptions::NotImplemented("GetSpecialContentIndex: Unknown content type {:#x}", content_type);
}
} else {
throw Mikage::Exceptions::NotImplemented("GetSpecialContentIndex: Only media type 2 supported");
}
break;
}
case 0x83d: // CheckAuthorityToAccessExtSaveData
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x84, 1); // access allowed
break;
case GetPriority::id:
IPC::HandleIPCCommand<GetPriority>(BindMemFn(&FakeFS::HandleGetPriority, this), thread, thread);
break;
case GetFormatInfo::id:
IPC::HandleIPCCommand<GetFormatInfo>(BindMemFn(&FakeFS::HandleGetFormatInfo, this), thread, thread, session_pid);
break;
case 0x849:
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 5, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x88, 0x1000); // Sector size in bytes
thread.WriteTLS(0x8c, 0x1000); // Cluster size in bytes
thread.WriteTLS(0x90, 1024 * 100); // Partition capacity in clusters
thread.WriteTLS(0x94, 1024 * 100); // Free space in clusters
break;
case FormatSaveData::id:
IPC::HandleIPCCommand<FormatSaveData>(BindMemFn(&FakeFS::HandleFormatSaveData, this), thread, thread, session_pid);
break;
case UpdateSha256Context::id:
IPC::HandleIPCCommand<UpdateSha256Context>(BindMemFn(&FakeFS::HandleUpdateSha256Context, this), thread, thread, session_pid);
break;
case CreateExtSaveData::id:
IPC::HandleIPCCommand<CreateExtSaveData>(BindMemFn(&FakeFS::HandleCreateExtSaveData, this), thread, thread, session_pid);
break;
case DeleteExtSaveData::id:
IPC::HandleIPCCommand<DeleteExtSaveData>(BindMemFn(&FakeFS::HandleDeleteExtSaveData, this), thread, thread, session_pid);
break;
case CreateSystemSaveData::id:
IPC::HandleIPCCommand<CreateSystemSaveData>(BindMemFn(&FakeFS::HandleCreateSystemSaveData, this), thread, thread, session_pid);
break;
case DeleteSystemSaveData::id:
{
uint32_t save_data_info = thread.ReadTLS(0x84);
uint32_t save_id = thread.ReadTLS(0x88);
logger.info("{}received DeleteSystemSaveData with save_data_info={:#x}, save_id={:#x}",
ThreadPrinter{thread}, save_data_info, save_id);
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
case GetProgramLaunchInfo::id:
IPC::HandleIPCCommand<GetProgramLaunchInfo>(BindMemFn(&FakeFS::HandleGetProgramLaunchInfo, this), thread, thread);
break;
// Used by menu during boot
case GetCardType::id:
logger.info("{}received stubbed IPC command GetCardType", ThreadPrinter{thread});
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
// TODO: Consider returning error here if no card is inserted. This may help 4.5.0 display an appropriate message in that case
thread.WriteTLS(0x84, RESULT_OK);
// thread.WriteTLS(0x84, 0xc8804464);
thread.WriteTLS(0x88, 0); // CTR card
break;
// Used by ptm during boot
case Unknown0x839::id:
logger.info("{}received stubbed IPC command 0x839", ThreadPrinter{thread});
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
case 0x85d: // SetFsCompatibilityInfo
logger.info("{}received SetFsCompatibilityInfo", ThreadPrinter{thread});
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
case 0x85e: // ResetCardCompatibilityParameter
{
uint32_t parameter = thread.ReadTLS(0x84);
logger.info("{}received ResetCardCompatibilityParameter with parameter={:#x}",
ThreadPrinter{thread}, parameter);
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
case 0x862: // SetPriority
{
uint32_t priority = thread.ReadTLS(0x84);
logger.info("{}received SetPriority with priority={:#x}",
ThreadPrinter{thread}, priority);
// LogStub(header);
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
}
case 0x86e: // SetThisSaveDataSecureValue (Super Smash Bros)
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 1, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
break;
case 0x86f: // GetThisSaveDataSecureValue (Super Smash Bros)
thread.WriteTLS(0x80, IPC::CommandHeader::Make(0, 2, 0).raw);
thread.WriteTLS(0x84, RESULT_OK);
thread.WriteTLS(0x88, 0);
break;
default:
throw Mikage::Exceptions::NotImplemented("Unknown FS service command with header {:#010x}", header.raw);
}
return HLE::OS::ServiceHelper::SendReply;
} catch (const IPC::IPCError& exc) {
auto response_header = IPC::CommandHeader::Make(0, 1, 0);
response_header.command_id = (exc.header >> 16);
thread.WriteTLS(0x80, response_header.raw);
thread.WriteTLS(0x84, exc.result);
return HLE::OS::ServiceHelper::SendReply;
}
std::tuple<OS::Result> FakeFS::HandleInitialize(FakeThread& thread, Handle session_handle, ProcessId id) {
fsuser_process_ids.emplace(std::make_pair(session_handle, id));
return std::make_tuple(RESULT_OK);
}
std::tuple<OS::Result> FakeFS::HandleInitializeWithSdkVersion(FakeThread& thread, Handle session_handle, uint32_t version, ProcessId id) {
fsuser_process_ids.emplace(std::make_pair(session_handle, id));
return std::make_tuple(RESULT_OK);
}
// TODOTEST: What happens to files when their corresponding Archive is closed? (Presumably they stay alive)
// TODOTEST: What's the maximal path size? This is bound by the size of the static buffer given by path_addr!
std::tuple<OS::Result, Handle> FakeFS::HandleOpenFile(FakeThread& thread, ProcessId session_id, uint32_t transaction, uint64_t archive_handle, uint32_t path_type, uint32_t path_size, uint32_t open_flags, uint32_t attributes, IPC::StaticBuffer path) {
if (archives.count(archive_handle) == 0) {
throw std::runtime_error("Couldn't find the given archive handle");
}
auto& archive = archives[archive_handle];
if (path.size < path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
path.size = path_size;
auto file = Meta::invoke([&]() {
try {
auto result = archive->OpenFile(thread, *this, transaction, open_flags, attributes, path_type, path);
if (result.first != RESULT_OK) {
thread.GetLogger()->warn("Archive instance failed to open the given file");
throw IPC::IPCError { 0x08020040, result.first };
}
return std::move(result.second);
} catch (std::ios_base::failure& err) {
thread.GetLogger()->error("Unexpected fstream exception in OpenFile via archive {}: {}", boost::core::demangle(typeid(*archive).name()), IosExceptionInfo(err));
throw;
}
});
// Create file session object and append it to the file session handler thread
OS::Result result;
HandleTable::Entry<ServerSession> server_session;
HandleTable::Entry<ClientSession> client_session;
std::tie(result,server_session,client_session) = thread.CallSVC(&OS::OS::SVCCreateSession);
if (result != RESULT_OK) {
thread.GetLogger()->error("Failed to create file session");
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
server_session.second->name = "File_ServerSession";
client_session.second->name = "File_ClientSession";
service.Append(server_session, std::move(file));
return std::make_tuple(RESULT_OK, client_session.first);
}
std::tuple<OS::Result, Handle> FakeFS::HandleOpenFileDirectly(FakeThread& thread, ProcessId session_id,
uint32_t transaction, uint32_t archive_id,
uint32_t archive_path_type, uint32_t archive_path_size,
uint32_t file_path_type, uint32_t file_path_size,
uint32_t open_flags, uint32_t attributes,
const IPC::StaticBuffer& archive_path,
const IPC::StaticBuffer& file_path) {
OS::Result result;
uint32_t archive_handle;
std::tie(result, archive_handle) = HandleOpenArchive(thread, session_id, archive_id, archive_path_type, archive_path_size, archive_path);
if (result != RESULT_OK) {
logger.error("{}OpenArchive returned error code {:#x}", ThreadPrinter{thread}, result);
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
Handle client_session;
std::tie(result, client_session) = HandleOpenFile(thread, session_id, transaction, archive_handle, file_path_type, file_path_size, open_flags, attributes, file_path);
if (result != RESULT_OK) {
logger.error("{}OpenFile returned error code {:#x}", ThreadPrinter{thread}, result);
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
std::tie(result) = HandleCloseArchive(thread, session_id, archive_handle);
if (result != RESULT_OK) {
logger.error("{}CloseArchive returned error code {:#x}", ThreadPrinter{thread}, result);
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
return std::make_tuple(RESULT_OK, client_session);
}
std::tuple<OS::Result> FakeFS::HandleDeleteFile(FakeThread& thread, ProcessId session_id,
uint32_t transaction, uint64_t archive_handle,
uint32_t file_path_type, uint32_t file_path_size,
IPC::StaticBuffer file_path) {
if (archives.count(archive_handle) == 0) {
throw Mikage::Exceptions::Invalid("Attempted to delete file from unknown archive handle");
}
auto& archive = archives[archive_handle];
if (file_path.size < file_path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
// Restrict the path size to the given one
file_path.size = file_path_size;
// TODO: Assert that the file is not currently opened
auto result = archive->DeleteFile(thread, *this, transaction, file_path_type, file_path);
return std::make_tuple(result);
}
std::tuple<OS::Result> FakeFS::HandleRenameFile(
FakeThread& thread, uint32_t transaction,
uint64_t source_archive_handle, uint32_t source_file_path_type, uint32_t source_file_path_size,
uint64_t target_archive_handle, uint32_t target_file_path_type, uint32_t target_file_path_size,
IPC::StaticBuffer source_file_path, IPC::StaticBuffer target_file_path) {
if (source_archive_handle != target_archive_handle) {
throw Mikage::Exceptions::Invalid("May not rename files across archives");
}
if (archives.count(source_archive_handle) == 0) {
throw Mikage::Exceptions::Invalid("Attempted to rename file from unknown archive handle");
}
auto& archive = archives[source_archive_handle];
if (source_file_path.size < source_file_path_size) {
throw std::runtime_error("Given source path length is larger than the static buffer size");
}
if (target_file_path.size < target_file_path_size) {
throw std::runtime_error("Given target path length is larger than the static buffer size");
}
auto result = archive->RenameFile(thread, *this, transaction, source_file_path_type, source_file_path, target_file_path_type, target_file_path);
return std::make_tuple(result);
}
std::tuple<OS::Result> FakeFS::HandleDeleteDirectory(FakeThread& thread, ProcessId session_id, bool recursive,
uint32_t transaction, ArchiveHandle archive_handle,
uint32_t dir_path_type, uint32_t dir_path_size,
IPC::StaticBuffer dir_path) {
if (archives.count(archive_handle) == 0)
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
auto& archive = archives[archive_handle];
if (dir_path.size < dir_path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
// Restrict the path size to the given one
dir_path.size = dir_path_size;
// TODO: Assert that the directory is not currently opened
auto result = archive->DeleteDirectory(thread, *this, transaction, recursive, dir_path_type, dir_path);
return std::make_tuple(result);
}
std::tuple<OS::Result> FakeFS::HandleCreateFile(FakeThread& thread, ProcessId session_id,
uint32_t transaction, uint64_t archive_handle,
uint32_t file_path_type, uint32_t file_path_size,
uint32_t attributes, uint64_t initial_file_size,
IPC::StaticBuffer file_path) {
if (file_path.size < file_path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
if (archives.count(archive_handle) == 0)
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
auto& archive = archives[archive_handle];
return archive->CreateFile(thread, *this, transaction, attributes, initial_file_size, file_path_type, file_path);
}
std::tuple<OS::Result> FakeFS::HandleCreateDirectory(FakeThread& thread, ProcessId /*session_id*/,
uint32_t transaction, uint64_t archive_handle,
uint32_t dir_path_type, uint32_t dir_path_size,
uint32_t /*attributes*/, IPC::StaticBuffer dir_path) {
if (dir_path.size < dir_path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
if (archives.count(archive_handle) == 0)
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
auto& archive = archives[archive_handle];
auto result = archive->CreateDirectory(thread, *this, dir_path_type, dir_path);
return std::make_tuple(result);
}
std::tuple<OS::Result, Handle> FakeFS::HandleOpenDirectory(FakeThread &thread, ProcessId session_id, uint64_t archive_handle, uint32_t path_type, uint32_t path_size, IPC::StaticBuffer path) {
if (archives.count(archive_handle) == 0) {
thread.GetLogger()->error("Couldn't find the given archive id");
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
auto& archive = archives[archive_handle];
if (path.size < path_size)
throw std::runtime_error("Given path length is larger than the static buffer size");
path.size = path_size;
auto dir = Meta::invoke([&]() {
try {
auto result = archive->OpenDirectory(thread, *this, path_type, path);
if (result.first != RESULT_OK) {
thread.GetLogger()->warn("Archive instance failed to open the given directory");
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
return std::move(result.second);
} catch (std::ios_base::failure& err) {
thread.GetLogger()->error("Unexpected fstream exception in OpenDirectory via archive {}: {}", boost::core::demangle(typeid(*archive).name()), IosExceptionInfo(err));
throw;
}
});
// Create file session object and append it to the file session handler thread
OS::Result result;
HandleTable::Entry<ServerSession> server_session;
HandleTable::Entry<ClientSession> client_session;
std::tie(result,server_session,client_session) = thread.CallSVC(&OS::OS::SVCCreateSession);
if (result != RESULT_OK) {
thread.GetLogger()->error("Failed to create directory session");
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
server_session.second->name = "Dir_ServerSession";
client_session.second->name = "Dir_ClientSession";
service.Append(server_session, std::move(dir));
return std::make_tuple(RESULT_OK, client_session.first);
}
/**
* Open the given archive without publically registering it.
* The archive is closed automatically on destruction.
*/
static std::tuple<OS::Result, std::unique_ptr<Archive>>
OpenArchive(FakeThread& thread, FSContext& context, ProcessId process_id, uint32_t archive_id, uint32_t path_type, uint32_t path_size, IPC::StaticBuffer path) try {
// TODO: This is conditional on session_id to avoid rejecting loader from accessing FS. How should this be done properly, though?
if (process_id > 5 && context.process_infos.count(process_id) == 0) {
throw std::runtime_error(fmt::format("Process {} is not registered to FS", process_id));
}
if (path_size > path.size)
throw std::runtime_error("Given path size is larger than the total static buffer size");
// Pass on the bounded path size
path.size = path_size;
path.id = 0;
switch (archive_id) {
case 0x00000003:
{
// TODO: Remove this legacy code.
auto program_info = context.process_infos.at(process_id).program;
std::cerr << "Title id: " << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id & 0xFFFFFFFF) << std::setw(8) << (program_info.program_id >> 32) << std::endl;
auto archive = std::unique_ptr<Archive>(new ArchiveOwnNCCHSection(thread, context, 0x2 /* path type = binary */, path, program_info));
return std::make_tuple(RESULT_OK, std::move(archive));
// auto program_info = process_infos[session_id].program;
// std::cerr << "Title id: " << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id & 0xFFFFFFFF) << std::setw(8) << (program_info.program_id >> 32) << std::endl;
//
// auto new_path = static_buffer_addr[0];
// TODOTOD: 12 bytes. thread.WriteMemory32(new_path, program_info.program_id & 0xFFFFFFFF);
// thread.WriteMemory32(new_path + 4, program_info.program_id >> 32);
// thread.WriteMemory32(new_path + 8, program_info.media_type);
// thread.WriteMemory32(new_path + 12, 0);
// thread.WriteMemory32(new_path + 16, 0);
// auto archive = std::make_unique<ArchivePXI>(thread, *this, 0x2345678a /* PXI archive */, 0x2 /* path type = binary */, IPC::StaticBuffer { new_path, 20, 0 });
// thread.GetParentProcess().FreeStaticBuffer(new_path);
// next_archive_handle++; // TODO: Move this into a MakeNewArchiveHandle function
// archives.emplace(std::make_pair(next_archive_handle, std::move(archive)));
//
// return std::make_tuple(RESULT_OK, next_archive_handle);
}
case 0x00000004:
{
// TODO: program info may not even be necessary...
auto program_info = context.process_infos.at(process_id).program;
std::cerr << "Title id: " << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id & 0xFFFFFFFF) << std::setw(8) << (program_info.program_id >> 32) << std::endl;
auto archive = std::unique_ptr<Archive>(new ArchiveSaveData(context, program_info));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x00000008:
{
auto archive = std::unique_ptr<Archive>(new ArchiveSystemSaveDataForSaveId(thread, context, path_type, path));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x00000009:
{
auto archive = std::unique_ptr<Archive>(new ArchiveHostDir(HostSdmcDirectory()));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x00000006:
case 0x00000007:
{
// NOTE: Home Menu uses this with mediatype 00000000 and extdata id 000000e000000000.
// Apparently, when we return success in this case, Home Menu boots into the System Transfer title (CARDBOAR)
// TODO: Verify path size
auto media_type = Serialization::LoadVia<uint32_t>(thread, path.addr);
auto extdata_id = Serialization::LoadVia<uint64_t>(thread, path.addr + 4);
auto extdata_info = ExtSaveDataInfo { static_cast<Platform::FS::MediaType>(media_type), {}, extdata_id, 0 };
bool is_shared = (archive_id == 0x7);
auto archive = std::unique_ptr<Archive>(new ArchiveExtSaveData(thread, context, extdata_info, is_shared, false));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x12345678:
{
auto archive = std::unique_ptr<Archive>(new ArchiveDummy(thread, context, path_type, path, archive_id));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x1234567d:
{
auto archive = std::unique_ptr<Archive>(new ArchiveHostDir(GetRootDataDirectory() / "rw"));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x1234567e:
{
auto archive = std::unique_ptr<Archive>(new ArchiveHostDir(GetRootDataDirectory() / "ro"));
return std::make_tuple(RESULT_OK, std::move(archive));
}
case 0x2345678a:
case 0x2345678e:
{
auto archive = std::unique_ptr<Archive>(new ArchiveNCCHSection(thread, context, archive_id, path_type, path));
return std::make_tuple(RESULT_OK, std::move(archive));
}
// TWL photo. Opened by mset during initial system setup
case 0x567890ac:
{
auto archive = std::unique_ptr<Archive>(new ArchiveHostDir(GetRootDataDirectory() / "twlp"));
return std::make_tuple(RESULT_OK, std::move(archive));
}
// TODO: Implement other archives
default:
throw std::runtime_error(fmt::format("FS received OpenArchive on unknown archive ID {:#x}", archive_id));
}
} catch (std::ios_base::failure& err) {
thread.GetLogger()->error("Unexpected fstream exception in HandleOpenArchive: {}",
IosExceptionInfo(err));
throw;
}
std::tuple<OS::Result, uint64_t> FakeFS::HandleOpenArchive(FakeThread& thread, ProcessId session_id, uint32_t archive_id, uint32_t path_type, uint32_t path_size, IPC::StaticBuffer path) {
auto [result, archive] = OpenArchive(thread, *this, session_id, archive_id, path_type, path_size, path);
// Move archive pointer into list and create an archive handle to refer to it
next_archive_handle++;
archives.emplace(std::make_pair(next_archive_handle, std::move(archive)));
return std::make_tuple(RESULT_OK, next_archive_handle);
}
std::tuple<OS::Result, IPC::MappedBuffer, IPC::MappedBuffer> FakeFS::HandleControlArchive(FakeThread& thread, ProcessId session_id, uint64_t archive_handle, uint32_t action, uint32_t input_size, uint32_t output_size, IPC::MappedBuffer input, IPC::MappedBuffer output) {
logger.info("{}received ControlArchive with archive_handle={:#x}, action={:#x}, input_size={:#x}, output_size={:#x}",
ThreadPrinter{thread}, archive_handle, action, input_size, output_size);
if (action == 0) {
// Commit save data changes. We implement this as a nop for now (TODO).
} else if (action == 1) {
// Time stamp of last modification. We implement this as a nop for now (TODO).
} else {
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
}
return std::make_tuple(RESULT_OK, input, output);
}
std::tuple<OS::Result> FakeFS::HandleCloseArchive(FakeThread& thread, ProcessId session_id, uint64_t archive_handle) {
logger.info("{}received CloseArchive with archive_handle={:#x}",
ThreadPrinter{thread}, archive_handle);
return std::make_tuple(RESULT_OK);
}
std::tuple<OS::Result> FakeFS::HandleFormatOwnSaveData(FakeThread& thread, ProcessId session_id, uint32_t block_size,
uint32_t num_dirs, uint32_t num_files, uint32_t num_dir_buckets,
uint32_t num_file_buckets, uint32_t unknown) {
return HandleFormatSaveData(thread, session_id, 0x4, 1 /* PATH_EMPTY */, 1, block_size,
num_dirs, num_files, num_dir_buckets, num_file_buckets, unknown,
IPC::StaticBuffer { static_buffer_addr[0], static_buffer_size, 0 });
}
std::tuple<OS::Result, uint32_t> FakeFS::HandleIsSdmcDetected(FakeThread& thread) {
logger.info("{}received IsSdmcDetected", ThreadPrinter{thread});
// TODO: Make this configurable
uint32_t detected = 1;
// uint32_t detected = 0;
return std::make_tuple(RESULT_OK, detected);
}
std::tuple<OS::Result, uint32_t> FakeFS::HandleGetPriority(FakeThread& thread) {
logger.info("{}received GetPriority", ThreadPrinter{thread});
uint32_t stub_priority = 0;
return std::make_tuple(RESULT_OK, stub_priority);
}
std::tuple<OS::Result, Platform::PXI::PM::ProgramInfo> FakeFS::HandleGetProgramLaunchInfo(FakeThread& thread, ProcessId id) {
logger.info("{}received GetProgramLaunchInfo for process id {}", ThreadPrinter{thread}, id);
auto it = process_infos.find(id);
if (it == process_infos.end())
thread.CallSVC(&OS::OS::SVCBreak, OS::OS::BreakReason::Panic);
return std::make_tuple(RESULT_OK, it->second.program);
}
std::tuple<OS::Result, ArchiveFormatInfo>
FakeFS::HandleGetFormatInfo(FakeThread& thread, ProcessId session_id, Platform::FS::ArchiveId archive_id,
uint32_t archive_path_type, uint32_t archive_path_size, IPC::StaticBuffer archive_path) {
auto [result, archive] = OpenArchive(thread, *this, session_id, archive_id, archive_path_type, archive_path_size, archive_path);
if (result != RESULT_OK) {
throw std::runtime_error(fmt::format("Failed to open archive {:#x} for GetFormatInfo", archive_id));
}
auto info = archive->GetFormatInfo(thread, *this);
return std::make_tuple(RESULT_OK, info);
}
std::tuple<OS::Result> FakeFS::HandleFormatSaveData(FakeThread&, ProcessId session_id, ArchiveId archive_id,
uint32_t path_type, uint32_t, uint32_t max_size,
uint32_t max_directories, uint32_t max_files,
uint32_t, uint32_t, uint32_t duplicate_data, IPC::StaticBuffer) {
if (archive_id != 4) {
throw std::runtime_error(fmt::format("Expected archive id 0x4 in FormatSaveData, got {:#x}", archive_id));
}
if (path_type != 1 /* EMPTY */) {
throw std::runtime_error("Expected empty path in FormatSaveData");
}
auto it = process_infos.find(session_id);
if (it == process_infos.end()) {
throw std::runtime_error(fmt::format("Called FormatSaveData from unregistered process with id {}", session_id));
}
auto format_info = ArchiveFormatInfo::IPCDeserialize(max_size, max_directories, max_files, duplicate_data);
ArchiveSaveData::GetProvider(*this, it->second.program).Format(format_info);
return std::make_tuple(RESULT_OK);
}
std::tuple< OS::Result, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, IPC::MappedBuffer>
FakeFS::HandleUpdateSha256Context( FakeThread& thread, ProcessId, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t num_bytes, uint32_t, uint32_t,
uint32_t, uint32_t, IPC::MappedBuffer data) {
if (data.size < num_bytes) {
throw Mikage::Exceptions::Invalid("Input buffer too small");
}
if ((data.size % 0x40) != 0) {
// throw Mikage::Exceptions::NotImplemented("Partial hashing not supported");
}
std::array<uint32_t, 8> result_sha {};
// Reset SHA256 hardware context
thread.WriteMemory32(0x1ec01000, 1);
// Compute hash
for (uint32_t i = 0; i < num_bytes; ++i) {
auto val = thread.ReadMemory(data.addr + i);
thread.WriteMemory(0x1ee01000 + i % 0x40, val);
}
// Finalize hash
thread.WriteMemory32(0x1ec01000, 2);
// Read result
for (uint32_t i = 0; i < result_sha.size(); ++i) {
// TODO: Have to use ReadPhysicalMemory32 here since ReadMemory32 expands to four individual 8-bit reads...
result_sha[i] = thread.GetParentProcess().ReadPhysicalMemory32(Memory::IO_HASH::start + 0x40 + i * sizeof(uint32_t));
}
return std::make_tuple( RESULT_OK, result_sha[0], result_sha[1], result_sha[2], result_sha[3], result_sha[4], result_sha[5], result_sha[6], result_sha[7], data);
}
std::tuple<OS::Result, IPC::MappedBuffer>
FakeFS::HandleCreateExtSaveData(FakeThread& thread, ProcessId, const Platform::FS::ExtSaveDataInfo& info,
uint32_t max_directories, uint32_t max_files, uint64_t, uint32_t,
IPC::MappedBuffer input_smdh) {
auto result = ArchiveExtSaveData::CreateSaveData(thread, *this, info, max_directories, max_files, true);
return std::make_tuple(result, input_smdh);
}
std::tuple<OS::Result, IPC::MappedBuffer>
FakeFS::HandleCreateExtSaveDataLegacy( FakeThread& thread, ProcessId process_id, uint32_t media_type,
uint64_t save_id, uint32_t smdh_size, uint32_t max_directories,
uint32_t max_files, IPC::MappedBuffer smdh) {
return HandleCreateExtSaveData( thread, process_id, ExtSaveDataInfo { static_cast<Platform::FS::MediaType>(media_type), {}, save_id, {} },
max_directories, max_files, std::numeric_limits<uint64_t>::max(),
smdh_size, smdh);
}
std::tuple<OS::Result> FakeFS::HandleDeleteExtSaveData(FakeThread& thread, ProcessId, const Platform::FS::ExtSaveDataInfo& info) {
auto result = ArchiveExtSaveData::DeleteSaveData(thread, *this, info, true);
return std::make_tuple(result);
}
std::tuple<OS::Result> FakeFS::HandleCreateSystemSaveData(FakeThread&, ProcessId, uint32_t media_type, uint32_t save_id,
uint32_t total_size, uint32_t /*block_size*/, uint32_t /*num_directories*/,
uint32_t /*num_files*/, uint32_t, uint32_t, uint32_t) {
if (media_type > Meta::to_underlying(MediaType::SD)) {
throw Mikage::Exceptions::Invalid("Invalid media type");
}
ArchiveSystemSaveDataForSaveId::CreateSystemSaveData(*this, static_cast<MediaType>(media_type), save_id, total_size);
return std::make_tuple(RESULT_OK);
}
void FakeFS::FSRegThread(FakeThread& thread) {
HLE::OS::ServiceHelper service;
service.Append(OS::ServiceUtil::SetupService(thread, "fs:REG", 2));
auto InvokeCommandHandler = [this](FakeThread& thread, uint32_t index) {
Platform::IPC::CommandHeader header = { thread.ReadTLS(0x80) };
return RegCommandHandler(thread, header);
};
service.Run(thread, std::move(InvokeCommandHandler));
}
decltype(HLE::OS::ServiceHelper::SendReply) FakeFS::RegCommandHandler(FakeThread& thread, IPC::CommandHeader header) {
// server_session: Incoming IPC command from the indexed client
thread.GetLogger()->info("{}received IPC request", ThreadPrinter{thread});
namespace FSReg = Platform::FS::Reg;
switch (header.command_id) {
case FSReg::Register::id:
IPC::HandleIPCCommand<FSReg::Register>(BindMemFn(&FakeFS::OnReg_Register, this), thread, thread);
break;
case FSReg::Unregister::id:
IPC::HandleIPCCommand<FSReg::Unregister>(BindMemFn(&FakeFS::OnReg_Unregister, this), thread, thread);
break;
case FSReg::CheckHostLoadId::id:
IPC::HandleIPCCommand<FSReg::CheckHostLoadId>(BindMemFn(&FakeFS::OnReg_CheckHostLoadId, this), thread, thread);
break;
default:
throw Mikage::Exceptions::NotImplemented( "{}received unknown command id {:#x} (full command: {:#010x})",
ThreadPrinter{thread}, header.command_id.Value(), header.raw);
}
return HLE::OS::ServiceHelper::SendReply;
}
OS::OS::ResultAnd<> FakeFS::OnReg_Register(FakeThread& thread, ProcessId pid, Platform::PXI::PM::ProgramHandle program_handle, Platform::PXI::PM::ProgramInfo program_info, Platform::FS::StorageInfo storage_info) {
auto info = ProcessInfo { program_info, storage_info };
process_infos.emplace(std::make_pair(pid, info));
return std::make_tuple(RESULT_OK);
}
OS::OS::ResultAnd<> FakeFS::OnReg_Unregister(FakeThread&, ProcessId pid) {
auto entry_it = process_infos.find(pid);
if (entry_it == process_infos.end()) {
throw Mikage::Exceptions::Invalid("Tried to unregister process that hadn't been registered before");
}
process_infos.erase(entry_it);
return std::make_tuple(RESULT_OK);
}
OS::OS::ResultAnd<> FakeFS::OnReg_CheckHostLoadId(FakeThread& thread, Platform::PXI::PM::ProgramHandle program_handle) {
// This is related to "Host IO", which is only available on dev kits
logger.info("{}received CheckHostLoadId for program handle {:#018x}: Stub",
ThreadPrinter{thread}, program_handle.value);
// Return an error since we generally emulate a retail 3DS
return std::make_tuple(0xd9004677);
}
} // namespace HLE