/* * Copyright (c) 2018-2019 Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "../amsmitm_fs_utils.hpp" #include "fsmitm_romfs.hpp" namespace ams::mitm::fs { using namespace ams::fs; namespace romfs { namespace { constexpr u32 EmptyEntry = 0xFFFFFFFF; constexpr size_t FilePartitionOffset = 0x200; struct Header { s64 header_size; s64 dir_hash_table_ofs; s64 dir_hash_table_size; s64 dir_table_ofs; s64 dir_table_size; s64 file_hash_table_ofs; s64 file_hash_table_size; s64 file_table_ofs; s64 file_table_size; s64 file_partition_ofs; }; static_assert(std::is_pod
::value && sizeof(Header) == 0x50); struct DirectoryEntry { u32 parent; u32 sibling; u32 child; u32 file; u32 hash; u32 name_size; char name[]; }; static_assert(std::is_pod::value && sizeof(DirectoryEntry) == 0x18); struct FileEntry { u32 parent; u32 sibling; s64 offset; s64 size; u32 hash; u32 name_size; char name[]; }; static_assert(std::is_pod::value && sizeof(FileEntry) == 0x20); constexpr inline DirectoryEntry *GetDirectoryEntry(void *dir_table, u32 offset) { return reinterpret_cast(reinterpret_cast(dir_table) + offset); } constexpr inline FileEntry *GetFileEntry(void *file_table, u32 offset) { return reinterpret_cast(reinterpret_cast(file_table) + offset); } constexpr inline const DirectoryEntry *GetDirectoryEntry(const void *dir_table, u32 offset) { return reinterpret_cast(reinterpret_cast(dir_table) + offset); } constexpr inline const FileEntry *GetFileEntry(const void *file_table, u32 offset) { return reinterpret_cast(reinterpret_cast(file_table) + offset); } constexpr inline u32 CalculatePathHash(u32 parent, const char *_path, u32 start, size_t path_len) { const unsigned char *path = reinterpret_cast(_path); u32 hash = parent ^ 123456789; for (size_t i = 0; i < path_len; i++) { hash = (hash >> 5) | (hash << 27); hash ^= path[start + i]; } return hash; } constexpr inline size_t GetHashTableSize(size_t num_entries) { if (num_entries < 3) { return 3; } else if (num_entries < 19) { return num_entries | 1; } else { size_t count = num_entries; while ((count % 2 == 0) || (count % 3 == 0) || (count % 5 == 0) || (count % 7 == 0) || (count % 11 == 0) || (count % 13 == 0) || (count % 17 == 0)) { count++; } return count; } } } Builder::Builder(ncm::ProgramId pr_id) : program_id(pr_id), num_dirs(0), num_files(0), dir_table_size(0), file_table_size(0), dir_hash_table_size(0), file_hash_table_size(0), file_partition_size(0) { auto res = this->directories.emplace("", std::make_unique(BuildDirectoryContext::RootTag{})); AMS_ASSERT(res.second); this->root = res.first->second.get(); this->num_dirs = 1; this->dir_table_size = 0x18; } void Builder::AddDirectory(BuildDirectoryContext **out, BuildDirectoryContext *parent_ctx, std::unique_ptr child_ctx) { /* Check if the directory already exists. */ auto existing = this->directories.find(child_ctx->path.get()); if (existing != this->directories.end()) { *out = existing->second.get(); return; } /* Add a new directory. */ this->num_dirs++; this->dir_table_size += sizeof(DirectoryEntry) + util::AlignUp(child_ctx->path_len - child_ctx->cur_path_ofs, 4); child_ctx->parent = parent_ctx; *out = child_ctx.get(); this->directories.emplace(child_ctx->path.get(), std::move(child_ctx)); } void Builder::AddFile(BuildDirectoryContext *parent_ctx, std::unique_ptr file_ctx) { /* Check if the file already exists. */ if (this->files.find(file_ctx->path.get()) != this->files.end()) { return; } /* Add a new file. */ this->num_files++; this->file_table_size += sizeof(FileEntry) + util::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4); file_ctx->parent = parent_ctx; this->files.emplace(file_ctx->path.get(), std::move(file_ctx)); } void Builder::VisitDirectory(FsFileSystem *fs, BuildDirectoryContext *parent) { FsDir dir; /* Get number of child directories. */ s64 num_child_dirs = 0; { R_ASSERT(mitm::fs::OpenAtmosphereRomfsDirectory(&dir, this->program_id, parent->path.get(), OpenDirectoryMode_Directory, fs)); ON_SCOPE_EXIT { fsDirClose(&dir); }; R_ASSERT(fsDirGetEntryCount(&dir, &num_child_dirs)); } AMS_ASSERT(num_child_dirs >= 0); { BuildDirectoryContext **child_dirs = reinterpret_cast(std::malloc(sizeof(BuildDirectoryContext *) * num_child_dirs)); ON_SCOPE_EXIT { std::free(child_dirs); }; AMS_ASSERT(child_dirs != nullptr); s64 cur_child_dir_ind = 0; R_ASSERT(mitm::fs::OpenAtmosphereRomfsDirectory(&dir, this->program_id, parent->path.get(), OpenDirectoryMode_All, fs)); { ON_SCOPE_EXIT { fsDirClose(&dir); }; s64 read_entries = 0; while (true) { R_ASSERT(fsDirRead(&dir, &read_entries, 1, &this->dir_entry)); if (read_entries != 1) { break; } AMS_ASSERT(this->dir_entry.type == FsDirEntryType_Dir || this->dir_entry.type == FsDirEntryType_File); if (this->dir_entry.type == FsDirEntryType_Dir) { BuildDirectoryContext *real_child = nullptr; this->AddDirectory(&real_child, parent, std::make_unique(parent->path.get(), parent->path_len, this->dir_entry.name, strlen(this->dir_entry.name))); AMS_ASSERT(real_child != nullptr); child_dirs[cur_child_dir_ind++] = real_child; AMS_ASSERT(cur_child_dir_ind <= num_child_dirs); } else /* if (this->dir_entry.type == FsDirEntryType_File) */ { this->AddFile(parent, std::make_unique(parent->path.get(), parent->path_len, this->dir_entry.name, strlen(this->dir_entry.name), this->dir_entry.file_size, 0, this->cur_source_type)); } } } AMS_ASSERT(num_child_dirs == cur_child_dir_ind); for (s64 i = 0; i < num_child_dirs; i++) { this->VisitDirectory(fs, child_dirs[i]); } } } void Builder::VisitDirectory(BuildDirectoryContext *parent, u32 parent_offset, const void *dir_table, size_t dir_table_size, const void *file_table, size_t file_table_size) { const DirectoryEntry *parent_entry = GetDirectoryEntry(dir_table, parent_offset); if (parent_entry->file != EmptyEntry) { const FileEntry *cur_file = GetFileEntry(file_table, parent_entry->file); while (true) { this->AddFile(parent, std::make_unique(parent->path.get(), parent->path_len, cur_file->name, cur_file->name_size, cur_file->size, cur_file->offset, this->cur_source_type)); if (cur_file->sibling == EmptyEntry) { break; } cur_file = GetFileEntry(file_table, cur_file->sibling); } } if (parent_entry->child != EmptyEntry) { const DirectoryEntry *cur_child = GetDirectoryEntry(dir_table, parent_entry->child); u32 cur_child_offset = parent_entry->child; while (true) { BuildDirectoryContext *real_child = nullptr; this->AddDirectory(&real_child, parent, std::make_unique(parent->path.get(), parent->path_len, cur_child->name, cur_child->name_size)); AMS_ASSERT(real_child != nullptr); this->VisitDirectory(real_child, cur_child_offset, dir_table, dir_table_size, file_table, file_table_size); if (cur_child->sibling == EmptyEntry) { break; } cur_child_offset = cur_child->sibling; cur_child = GetDirectoryEntry(dir_table, cur_child_offset); } } } void Builder::AddSdFiles() { /* Open Sd Card filesystem. */ FsFileSystem sd_filesystem; R_ASSERT(fsOpenSdCardFileSystem(&sd_filesystem)); ON_SCOPE_EXIT { fsFsClose(&sd_filesystem); }; /* If there is no romfs folder on the SD, don't bother continuing. */ { FsDir dir; if (R_FAILED(mitm::fs::OpenAtmosphereRomfsDirectory(&dir, this->program_id, this->root->path.get(), OpenDirectoryMode_Directory, &sd_filesystem))) { return; } fsDirClose(&dir); } this->cur_source_type = DataSourceType::LooseSdFile; this->VisitDirectory(&sd_filesystem, this->root); } void Builder::AddStorageFiles(ams::fs::IStorage *storage, DataSourceType source_type) { Header header; R_ASSERT(storage->Read(0, &header, sizeof(Header))); AMS_ASSERT(header.header_size == sizeof(Header)); /* Read tables. */ auto tables = std::unique_ptr(new u8[header.dir_table_size + header.file_table_size]); void *dir_table = tables.get(); void *file_table = tables.get() + header.dir_table_size; R_ASSERT(storage->Read(header.dir_table_ofs, dir_table, size_t(header.dir_table_size))); R_ASSERT(storage->Read(header.file_table_ofs, file_table, size_t(header.file_table_size))); this->cur_source_type = source_type; this->VisitDirectory(this->root, 0x0, dir_table, size_t(header.dir_table_size), file_table, size_t(header.file_table_size)); } void Builder::Build(std::vector *out_infos) { /* Clear output. */ out_infos->clear(); /* Open an SD card filesystem. */ FsFileSystem sd_filesystem; R_ASSERT(fsOpenSdCardFileSystem(&sd_filesystem)); ON_SCOPE_EXIT { fsFsClose(&sd_filesystem); }; /* Calculate hash table sizes. */ const size_t num_dir_hash_table_entries = GetHashTableSize(this->num_dirs); const size_t num_file_hash_table_entries = GetHashTableSize(this->num_files); this->dir_hash_table_size = sizeof(u32) * num_dir_hash_table_entries; this->file_hash_table_size = sizeof(u32) * num_file_hash_table_entries; /* Allocate metadata, make pointers. */ Header *header = reinterpret_cast
(std::malloc(sizeof(Header))); std::memset(header, 0x00, sizeof(*header)); const size_t metadata_size = this->dir_hash_table_size + this->dir_table_size + this->file_hash_table_size + this->file_table_size; std::unique_ptr metadata(new u8[metadata_size]); u32 *dir_hash_table = reinterpret_cast(metadata.get()); DirectoryEntry *dir_table = reinterpret_cast(reinterpret_cast(dir_hash_table) + this->dir_hash_table_size); u32 *file_hash_table = reinterpret_cast(reinterpret_cast(dir_table) + this->dir_table_size); FileEntry *file_table = reinterpret_cast(reinterpret_cast(file_hash_table) + this->file_hash_table_size); /* Clear hash tables. */ static_assert(EmptyEntry == 0xFFFFFFFF); std::memset(dir_hash_table, 0xFF, this->dir_hash_table_size); std::memset(file_hash_table, 0xFF, this->file_hash_table_size); /* Emplace metadata source info. */ out_infos->emplace_back(0, sizeof(*header), DataSourceType::Memory, header); /* Process Files. */ { u32 entry_offset = 0; BuildFileContext *cur_file = nullptr; BuildFileContext *prev_file = nullptr; for (const auto &it : this->files) { cur_file = it.second.get(); /* By default, pad to 0x10 alignment. */ this->file_partition_size = util::AlignUp(this->file_partition_size, 0x10); /* Check if extra padding is present in original source, preserve it to make our life easier. */ const bool is_storage_or_file = cur_file->source_type == DataSourceType::Storage || cur_file->source_type == DataSourceType::File; if (prev_file != nullptr && prev_file->source_type == cur_file->source_type && is_storage_or_file) { const s64 expected = this->file_partition_size - prev_file->offset + prev_file->orig_offset; if (expected != cur_file->orig_offset) { AMS_ASSERT(expected <= cur_file->orig_offset); this->file_partition_size += cur_file->orig_offset - expected; } } /* Calculate offsets. */ cur_file->offset = this->file_partition_size; this->file_partition_size += cur_file->size; cur_file->entry_offset = entry_offset; entry_offset += sizeof(FileEntry) + util::AlignUp(cur_file->path_len - cur_file->cur_path_ofs, 4); /* Save current file as prev for next iteration. */ prev_file = cur_file; } /* Assign deferred parent/sibling ownership. */ for (auto it = this->files.rbegin(); it != this->files.rend(); it++) { cur_file = it->second.get(); cur_file->sibling = cur_file->parent->file; cur_file->parent->file = cur_file; } } /* Process Directories. */ { u32 entry_offset = 0; BuildDirectoryContext *cur_dir = nullptr; for (const auto &it : this->directories) { cur_dir = it.second.get(); cur_dir->entry_offset = entry_offset; entry_offset += sizeof(DirectoryEntry) + util::AlignUp(cur_dir->path_len - cur_dir->cur_path_ofs, 4); } /* Assign deferred parent/sibling ownership. */ for (auto it = this->directories.rbegin(); it != this->directories.rend(); it++) { cur_dir = it->second.get(); if (cur_dir == this->root) { continue; } cur_dir->sibling = cur_dir->parent->child; cur_dir->parent->child = cur_dir; } } /* Populate file tables. */ for (const auto &it : this->files) { BuildFileContext *cur_file = it.second.get(); FileEntry *cur_entry = GetFileEntry(file_table, cur_file->entry_offset); /* Set entry fields. */ cur_entry->parent = cur_file->parent->entry_offset; cur_entry->sibling = (cur_file->sibling == nullptr) ? EmptyEntry : cur_file->sibling->entry_offset; cur_entry->offset = cur_file->offset; cur_entry->size = cur_file->size; /* Insert into hash table. */ const u32 name_size = cur_file->path_len - cur_file->cur_path_ofs; const size_t hash_ind = CalculatePathHash(cur_entry->parent, cur_file->path.get() + cur_file->cur_path_ofs, 0, name_size) % num_file_hash_table_entries; cur_entry->hash = file_hash_table[hash_ind]; file_hash_table[hash_ind] = cur_file->entry_offset; /* Set name. */ cur_entry->name_size = name_size; if (name_size) { std::memcpy(cur_entry->name, cur_file->path.get() + cur_file->cur_path_ofs, name_size); for (size_t i = name_size; i < util::AlignUp(name_size, 4); i++) { cur_entry->name[i] = 0; } } /* Emplace a source. */ switch (cur_file->source_type) { case DataSourceType::Storage: case DataSourceType::File: { /* Try to compact if possible. */ auto &back = out_infos->back(); if (back.source_type == cur_file->source_type) { back.size = cur_file->offset + FilePartitionOffset + cur_file->size - back.virtual_offset; } else { out_infos->emplace_back(cur_file->offset + FilePartitionOffset, cur_file->size, cur_file->source_type, cur_file->orig_offset + FilePartitionOffset); } } break; case DataSourceType::LooseSdFile: { out_infos->emplace_back(cur_file->offset + FilePartitionOffset, cur_file->size, cur_file->source_type, cur_file->path.release()); } break; AMS_UNREACHABLE_DEFAULT_CASE(); } } /* Populate directory tables. */ for (const auto &it : this->directories) { BuildDirectoryContext *cur_dir = it.second.get(); DirectoryEntry *cur_entry = GetDirectoryEntry(dir_table, cur_dir->entry_offset); /* Set entry fields. */ cur_entry->parent = cur_dir == this->root ? 0 : cur_dir->parent->entry_offset; cur_entry->sibling = (cur_dir->sibling == nullptr) ? EmptyEntry : cur_dir->sibling->entry_offset; cur_entry->child = (cur_dir->child == nullptr) ? EmptyEntry : cur_dir->child->entry_offset; cur_entry->file = (cur_dir->file == nullptr) ? EmptyEntry : cur_dir->file->entry_offset; /* Insert into hash table. */ const u32 name_size = cur_dir->path_len - cur_dir->cur_path_ofs; const size_t hash_ind = CalculatePathHash(cur_entry->parent, cur_dir->path.get() + cur_dir->cur_path_ofs, 0, name_size) % num_dir_hash_table_entries; cur_entry->hash = dir_hash_table[hash_ind]; dir_hash_table[hash_ind] = cur_dir->entry_offset; /* Set name. */ cur_entry->name_size = name_size; if (name_size) { std::memcpy(cur_entry->name, cur_dir->path.get() + cur_dir->cur_path_ofs, name_size); for (size_t i = name_size; i < util::AlignUp(name_size, 4); i++) { cur_entry->name[i] = 0; } } } /* Delete maps. */ this->root = nullptr; this->directories.clear(); this->files.clear(); /* Set header fields. */ header->header_size = sizeof(*header); header->file_hash_table_size = this->file_hash_table_size; header->file_table_size = this->file_table_size; header->dir_hash_table_size = this->dir_hash_table_size; header->dir_table_size = this->dir_table_size; header->file_partition_ofs = FilePartitionOffset; header->dir_hash_table_ofs = util::AlignUp(FilePartitionOffset + this->file_partition_size, 4); header->dir_table_ofs = header->dir_hash_table_ofs + header->dir_hash_table_size; header->file_hash_table_ofs = header->dir_table_ofs + header->dir_table_size; header->file_table_ofs = header->file_hash_table_ofs + header->file_hash_table_size; /* Save metadata to the SD card, to save on memory space. */ { FsFile metadata_file; R_ASSERT(mitm::fs::SaveAtmosphereSdFile(&metadata_file, this->program_id, "romfs_metadata.bin", metadata.get(), metadata_size)); out_infos->emplace_back(header->dir_hash_table_ofs, metadata_size, DataSourceType::Metadata, new RemoteFile(metadata_file)); } } } }