Merge branch 'master' into d3d12
This commit is contained in:
commit
f54dddee90
|
@ -18,7 +18,7 @@
|
||||||
url = https://github.com/xenia-project/SPIRV-Tools.git
|
url = https://github.com/xenia-project/SPIRV-Tools.git
|
||||||
[submodule "third_party/catch"]
|
[submodule "third_party/catch"]
|
||||||
path = third_party/catch
|
path = third_party/catch
|
||||||
url = https://github.com/philsquared/Catch.git
|
url = https://github.com/catchorg/Catch2.git
|
||||||
[submodule "third_party/gflags"]
|
[submodule "third_party/gflags"]
|
||||||
path = third_party/gflags
|
path = third_party/gflags
|
||||||
url = https://github.com/benvanik/gflags.git
|
url = https://github.com/benvanik/gflags.git
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/xenia-project/xenia/tree/master/assets/icon">
|
<a href="https://github.com/xenia-project/xenia/tree/master/assets/icon">
|
||||||
<img height="370px" src="https://raw.githubusercontent.com/xenia-project/xenia/master/assets/icon/1024.png" />
|
<img height="120px" src="https://raw.githubusercontent.com/xenia-project/xenia/master/assets/icon/128.png" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ int xenia_main(const std::vector<std::wstring>& args) {
|
||||||
#if defined(XE_PLATFORM_WIN32)
|
#if defined(XE_PLATFORM_WIN32)
|
||||||
content_root = xe::join_paths(content_root, L"Xenia");
|
content_root = xe::join_paths(content_root, L"Xenia");
|
||||||
#elif defined(XE_PLATFORM_LINUX)
|
#elif defined(XE_PLATFORM_LINUX)
|
||||||
content_root = xe::join_paths(content_root, L".xenia");
|
content_root = xe::join_paths(content_root, L"Xenia");
|
||||||
#else
|
#else
|
||||||
#warning Unhandled platform for content root.
|
#warning Unhandled platform for content root.
|
||||||
content_root = xe::join_paths(content_root, L"Xenia");
|
content_root = xe::join_paths(content_root, L"Xenia");
|
||||||
|
|
|
@ -116,6 +116,7 @@ struct FileInfo {
|
||||||
};
|
};
|
||||||
Type type;
|
Type type;
|
||||||
std::wstring name;
|
std::wstring name;
|
||||||
|
std::wstring path;
|
||||||
size_t total_size;
|
size_t total_size;
|
||||||
uint64_t create_timestamp;
|
uint64_t create_timestamp;
|
||||||
uint64_t access_timestamp;
|
uint64_t access_timestamp;
|
||||||
|
|
|
@ -12,29 +12,58 @@
|
||||||
#include "xenia/base/logging.h"
|
#include "xenia/base/logging.h"
|
||||||
#include "xenia/base/string.h"
|
#include "xenia/base/string.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <ftw.h>
|
#include <ftw.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace filesystem {
|
namespace filesystem {
|
||||||
|
|
||||||
std::wstring GetExecutablePath() {
|
std::wstring GetExecutablePath() {
|
||||||
assert_always(); // IMPLEMENT ME.
|
char buff[FILENAME_MAX] = "";
|
||||||
return std::wstring();
|
readlink("/proc/self/exe", buff, FILENAME_MAX);
|
||||||
|
std::string s(buff);
|
||||||
|
return to_wstring(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring GetExecutableFolder() {
|
std::wstring GetExecutableFolder() {
|
||||||
assert_always(); // IMPLEMENT ME.
|
auto path = GetExecutablePath();
|
||||||
return std::wstring();
|
return xe::find_base_path(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring GetUserFolder() {
|
std::wstring GetUserFolder() {
|
||||||
assert_always(); // IMPLEMENT ME.
|
// get preferred data home
|
||||||
return std::wstring();
|
char* dataHome = std::getenv("XDG_DATA_HOME");
|
||||||
|
|
||||||
|
// if XDG_DATA_HOME not set, fallback to HOME directory
|
||||||
|
if (dataHome == NULL) {
|
||||||
|
dataHome = std::getenv("HOME");
|
||||||
|
} else {
|
||||||
|
std::string home(dataHome);
|
||||||
|
return to_wstring(home);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if HOME not set, fall back to this
|
||||||
|
if (dataHome == NULL) {
|
||||||
|
struct passwd pw1;
|
||||||
|
struct passwd* pw;
|
||||||
|
char buf[4096]; // could potentionally lower this
|
||||||
|
getpwuid_r(getuid(), &pw1, buf, sizeof(buf), &pw);
|
||||||
|
assert(&pw1 == pw); // sanity check
|
||||||
|
dataHome = pw->pw_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string home(dataHome);
|
||||||
|
return to_wstring(home + "/.local/share");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathExists(const std::wstring& path) {
|
bool PathExists(const std::wstring& path) {
|
||||||
|
|
|
@ -207,6 +207,7 @@ bool GetInfo(const std::wstring& path, FileInfo* out_info) {
|
||||||
out_info->total_size =
|
out_info->total_size =
|
||||||
(data.nFileSizeHigh * (size_t(MAXDWORD) + 1)) + data.nFileSizeLow;
|
(data.nFileSizeHigh * (size_t(MAXDWORD) + 1)) + data.nFileSizeLow;
|
||||||
}
|
}
|
||||||
|
out_info->path = xe::find_base_path(path);
|
||||||
out_info->name = xe::find_name_from_path(path);
|
out_info->name = xe::find_name_from_path(path);
|
||||||
out_info->create_timestamp = COMBINE_TIME(data.ftCreationTime);
|
out_info->create_timestamp = COMBINE_TIME(data.ftCreationTime);
|
||||||
out_info->access_timestamp = COMBINE_TIME(data.ftLastAccessTime);
|
out_info->access_timestamp = COMBINE_TIME(data.ftLastAccessTime);
|
||||||
|
@ -236,6 +237,7 @@ std::vector<FileInfo> ListFiles(const std::wstring& path) {
|
||||||
info.total_size =
|
info.total_size =
|
||||||
(ffd.nFileSizeHigh * (size_t(MAXDWORD) + 1)) + ffd.nFileSizeLow;
|
(ffd.nFileSizeHigh * (size_t(MAXDWORD) + 1)) + ffd.nFileSizeLow;
|
||||||
}
|
}
|
||||||
|
info.path = path;
|
||||||
info.name = ffd.cFileName;
|
info.name = ffd.cFileName;
|
||||||
info.create_timestamp = COMBINE_TIME(ffd.ftCreationTime);
|
info.create_timestamp = COMBINE_TIME(ffd.ftCreationTime);
|
||||||
info.access_timestamp = COMBINE_TIME(ffd.ftLastAccessTime);
|
info.access_timestamp = COMBINE_TIME(ffd.ftLastAccessTime);
|
||||||
|
|
|
@ -9,12 +9,27 @@
|
||||||
|
|
||||||
#include "xenia/base/string.h"
|
#include "xenia/base/string.h"
|
||||||
|
|
||||||
// TODO(benvanik): when GCC finally gets codecvt, use that.
|
// codecvt existence check
|
||||||
#if XE_PLATFORM_LINUX
|
#ifdef __clang__
|
||||||
|
// using clang
|
||||||
|
#if (__clang_major__ < 4) // 3.3 has it but I think we need at least 4 anyway
|
||||||
|
// insufficient clang version
|
||||||
#define NO_CODECVT 1
|
#define NO_CODECVT 1
|
||||||
#else
|
#else
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
#endif // XE_PLATFORM_LINUX
|
#endif
|
||||||
|
#elif defined(__GNUC__) || defined(__GNUG__)
|
||||||
|
// using gcc
|
||||||
|
#if (__GNUC__ < 5)
|
||||||
|
// insufficient clang version
|
||||||
|
#define NO_CODECVT 1
|
||||||
|
#else
|
||||||
|
#include <codecvt>
|
||||||
|
#endif
|
||||||
|
// since the windows 10 sdk is required, this shouldn't be an issue
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
#include <codecvt>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include "xenia/vfs/devices/stfs_container_device.h"
|
#include "xenia/vfs/devices/stfs_container_device.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <queue>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "xenia/base/logging.h"
|
#include "xenia/base/logging.h"
|
||||||
|
@ -59,87 +60,88 @@ StfsContainerDevice::StfsContainerDevice(const std::string& mount_path,
|
||||||
StfsContainerDevice::~StfsContainerDevice() = default;
|
StfsContainerDevice::~StfsContainerDevice() = default;
|
||||||
|
|
||||||
bool StfsContainerDevice::Initialize() {
|
bool StfsContainerDevice::Initialize() {
|
||||||
if (filesystem::IsFolder(local_path_)) {
|
// Resolve a valid STFS file if a directory is given.
|
||||||
// Was given a folder. Try to find the file in
|
if (filesystem::IsFolder(local_path_) && !ResolveFromFolder(local_path_)) {
|
||||||
// local_path\TITLE_ID\000D0000\HASH_OF_42_CHARS
|
XELOGE("Could not resolve an STFS container given path %s",
|
||||||
// We take care to not die if there are additional files around.
|
xe::to_string(local_path_).c_str());
|
||||||
bool found_alternative = false;
|
return false;
|
||||||
auto files = filesystem::ListFiles(local_path_);
|
|
||||||
for (auto& file : files) {
|
|
||||||
if (file.type != filesystem::FileInfo::Type::kDirectory ||
|
|
||||||
file.name.size() != 8) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto child_path = xe::join_paths(local_path_, file.name);
|
|
||||||
auto child_files = filesystem::ListFiles(child_path);
|
|
||||||
for (auto& child_file : child_files) {
|
|
||||||
if (child_file.type != filesystem::FileInfo::Type::kDirectory ||
|
|
||||||
child_file.name != L"000D0000") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto stfs_path = xe::join_paths(child_path, child_file.name);
|
|
||||||
auto stfs_files = filesystem::ListFiles(stfs_path);
|
|
||||||
for (auto& stfs_file : stfs_files) {
|
|
||||||
if (stfs_file.type != filesystem::FileInfo::Type::kFile ||
|
|
||||||
stfs_file.name.size() != 42) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Probably it!
|
|
||||||
local_path_ = xe::join_paths(stfs_path, stfs_file.name);
|
|
||||||
found_alternative = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (found_alternative) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (found_alternative) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filesystem::PathExists(local_path_)) {
|
if (!filesystem::PathExists(local_path_)) {
|
||||||
XELOGE("STFS container does not exist");
|
XELOGE("Path to STFS container does not exist: %s",
|
||||||
|
xe::to_string(local_path_).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mmap_ = MappedMemory::Open(local_path_, MappedMemory::Mode::kRead);
|
// Map the data file(s)
|
||||||
if (!mmap_) {
|
auto map_result = MapFiles();
|
||||||
XELOGE("STFS container could not be mapped");
|
if (map_result != Error::kSuccess) {
|
||||||
return false;
|
XELOGE("Failed to map STFS container: %d", map_result);
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t* map_ptr = mmap_->data();
|
|
||||||
|
|
||||||
auto result = ReadHeaderAndVerify(map_ptr);
|
|
||||||
if (result != Error::kSuccess) {
|
|
||||||
XELOGE("STFS header read/verification failed: %d", result);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (header_.descriptor_type) {
|
switch (header_.descriptor_type) {
|
||||||
case StfsDescriptorType::kStfs:
|
case StfsDescriptorType::kStfs:
|
||||||
result = ReadAllEntriesSTFS(map_ptr);
|
return ReadSTFS() == Error::kSuccess;
|
||||||
break;
|
break;
|
||||||
case StfsDescriptorType::kSvod:
|
case StfsDescriptorType::kSvod:
|
||||||
if (!(header_.svod_volume_descriptor.device_features &
|
return ReadSVOD() == Error::kSuccess;
|
||||||
kFeatureHasEnhancedGDFLayout)) {
|
|
||||||
XELOGE("STFS SVOD does not have GDF layout!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ReadAllEntriesEGDF(map_ptr);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// Shouldn't reach here.
|
XELOGE("Unknown STFS Descriptor Type: %d", header_.descriptor_type);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (result != Error::kSuccess) {
|
}
|
||||||
XELOGE("STFS entry reading failed: %d", result);
|
|
||||||
return false;
|
StfsContainerDevice::Error StfsContainerDevice::MapFiles() {
|
||||||
|
// Map the file containing the STFS Header and read it.
|
||||||
|
XELOGI("Mapping STFS Header File: %s", xe::to_string(local_path_).c_str());
|
||||||
|
auto header_map = MappedMemory::Open(local_path_, MappedMemory::Mode::kRead);
|
||||||
|
|
||||||
|
auto header_result = ReadHeaderAndVerify(header_map->data());
|
||||||
|
if (header_result != Error::kSuccess) {
|
||||||
|
XELOGE("Error reading STFS Header: %d", header_result);
|
||||||
|
return header_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// If the STFS package is a single file, the header is self contained and
|
||||||
|
// we don't need to map any extra files.
|
||||||
|
// NOTE: data_file_count is 0 for STFS and 1 for SVOD
|
||||||
|
if (header_.data_file_count <= 1) {
|
||||||
|
XELOGI("STFS container is a single file.");
|
||||||
|
mmap_.emplace(std::make_pair(0, std::move(header_map)));
|
||||||
|
return Error::kSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the STFS package is multi-file, it is an SVOD system. We need to map
|
||||||
|
// the files in the .data folder and can discard the header.
|
||||||
|
auto data_fragment_path = local_path_ + L".data";
|
||||||
|
if (!filesystem::PathExists(data_fragment_path)) {
|
||||||
|
XELOGE("STFS container is multi-file, but path %s does not exist.",
|
||||||
|
xe::to_string(data_fragment_path).c_str());
|
||||||
|
return Error::kErrorFileMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure data fragment files are sorted
|
||||||
|
auto fragment_files = filesystem::ListFiles(data_fragment_path);
|
||||||
|
std::sort(fragment_files.begin(), fragment_files.end(),
|
||||||
|
[](filesystem::FileInfo& left, filesystem::FileInfo& right) {
|
||||||
|
return left.name < right.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fragment_files.size() != header_.data_file_count) {
|
||||||
|
XELOGE("SVOD expecting %d data fragments, but %d are present.",
|
||||||
|
header_.data_file_count, fragment_files.size());
|
||||||
|
return Error::kErrorFileMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < fragment_files.size(); i++) {
|
||||||
|
auto file = fragment_files.at(i);
|
||||||
|
auto path = xe::join_paths(file.path, file.name);
|
||||||
|
auto data = MappedMemory::Open(path, MappedMemory::Mode::kRead);
|
||||||
|
mmap_.emplace(std::make_pair(i, std::move(data)));
|
||||||
|
}
|
||||||
|
XELOGI("SVOD successfully mapped %d files.", fragment_files.size());
|
||||||
|
return Error::kSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StfsContainerDevice::Dump(StringBuffer* string_buffer) {
|
void StfsContainerDevice::Dump(StringBuffer* string_buffer) {
|
||||||
|
@ -196,87 +198,189 @@ StfsContainerDevice::Error StfsContainerDevice::ReadHeaderAndVerify(
|
||||||
return Error::kSuccess;
|
return Error::kSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesEGDF(
|
StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
|
||||||
const uint8_t* map_ptr) {
|
// SVOD Systems can have different layouts. The root block is
|
||||||
// Verify (and scan) the GDF magic first.
|
// denoted by the magic "MICROSOFT*XBOX*MEDIA" and is always in
|
||||||
const uint8_t* p = map_ptr + BlockToOffsetSTFS(0);
|
// the first "actual" data fragment of the system.
|
||||||
if (std::memcmp(p, "MICROSOFT*XBOX*MEDIA", 20) != 0) {
|
auto data = mmap_.at(0)->data();
|
||||||
return Error::kErrorDamagedFile;
|
const char* MEDIA_MAGIC = "MICROSOFT*XBOX*MEDIA";
|
||||||
|
|
||||||
|
// Check for EDGF layout
|
||||||
|
auto layout = &header_.svod_volume_descriptor.layout_type;
|
||||||
|
auto features = header_.svod_volume_descriptor.device_features;
|
||||||
|
bool has_egdf_layout = features & kFeatureHasEnhancedGDFLayout;
|
||||||
|
|
||||||
|
if (has_egdf_layout) {
|
||||||
|
// The STFS header has specified that this SVOD system uses the EGDF layout.
|
||||||
|
// We can expect the magic block to be located immediately after the hash
|
||||||
|
// blocks. We also offset block address calculation by 0x1000 by shifting
|
||||||
|
// block indices by +0x2.
|
||||||
|
if (memcmp(data + 0x2000, MEDIA_MAGIC, 20) == 0) {
|
||||||
|
base_offset_ = 0x0000;
|
||||||
|
magic_offset_ = 0x2000;
|
||||||
|
*layout = kEnhancedGDFLayout;
|
||||||
|
XELOGI("SVOD uses an EGDF layout. Magic block present at 0x2000.");
|
||||||
|
} else {
|
||||||
|
XELOGE("SVOD uses an EGDF layout, but the magic block was not found.");
|
||||||
|
return Error::kErrorFileMismatch;
|
||||||
|
}
|
||||||
|
} else if (memcmp(data + 0x12000, MEDIA_MAGIC, 20) == 0) {
|
||||||
|
// If the SVOD's magic block is at 0x12000, it is likely using an XSF
|
||||||
|
// layout. This is usually due to converting the game using a third-party
|
||||||
|
// tool, as most of them use a nulled XSF as a template.
|
||||||
|
|
||||||
|
base_offset_ = 0x10000;
|
||||||
|
magic_offset_ = 0x12000;
|
||||||
|
|
||||||
|
// Check for XSF Header
|
||||||
|
const char* XSF_MAGIC = "XSF";
|
||||||
|
if (memcmp(data + 0x2000, XSF_MAGIC, 3) == 0) {
|
||||||
|
*layout = kXSFLayout;
|
||||||
|
XELOGI("SVOD uses an XSF layout. Magic block present at 0x12000.");
|
||||||
|
XELOGI("Game was likely converted using a third-party tool.");
|
||||||
|
} else {
|
||||||
|
*layout = kUnknownLayout;
|
||||||
|
XELOGI("SVOD appears to use an XSF layout, but no header is present.");
|
||||||
|
XELOGI("SVOD magic block found at 0x12000");
|
||||||
|
}
|
||||||
|
} else if (memcmp(data + 0xD000, MEDIA_MAGIC, 20) == 0) {
|
||||||
|
// If the SVOD's magic block is at 0xD000, it most likely means that it is
|
||||||
|
// a single-file system. The STFS Header is 0xB000 bytes , and the remaining
|
||||||
|
// 0x2000 is from hash tables. In most cases, these will be STFS, not SVOD.
|
||||||
|
|
||||||
|
base_offset_ = 0xB000;
|
||||||
|
magic_offset_ = 0xD000;
|
||||||
|
|
||||||
|
// Check for single file system
|
||||||
|
if (header_.data_file_count == 1) {
|
||||||
|
*layout = kSingleFileLayout;
|
||||||
|
XELOGI("SVOD is a single file. Magic block present at 0xD000.");
|
||||||
|
} else {
|
||||||
|
*layout = kUnknownLayout;
|
||||||
|
XELOGE(
|
||||||
|
"SVOD is not a single file, but the magic block was found at "
|
||||||
|
"0xD000.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
XELOGE("Could not locate SVOD magic block.");
|
||||||
|
return Error::kErrorReadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t root_sector = xe::load<uint32_t>(p + 0x14);
|
// Parse the root directory
|
||||||
uint32_t root_size = xe::load<uint32_t>(p + 0x18);
|
uint8_t* magic_block = data + magic_offset_;
|
||||||
|
uint32_t root_block = xe::load<uint32_t>(magic_block + 0x14);
|
||||||
|
uint32_t root_size = xe::load<uint32_t>(magic_block + 0x18);
|
||||||
|
uint32_t root_creation_date = xe::load<uint32_t>(magic_block + 0x1C);
|
||||||
|
uint32_t root_creation_time = xe::load<uint32_t>(magic_block + 0x20);
|
||||||
|
uint64_t root_creation_timestamp =
|
||||||
|
decode_fat_timestamp(root_creation_date, root_creation_time);
|
||||||
|
|
||||||
auto root_entry = new StfsContainerEntry(this, nullptr, "", mmap_.get());
|
auto root_entry = new StfsContainerEntry(this, nullptr, "", &mmap_);
|
||||||
root_entry->attributes_ = kFileAttributeDirectory;
|
root_entry->attributes_ = kFileAttributeDirectory;
|
||||||
|
root_entry->access_timestamp_ = root_creation_timestamp;
|
||||||
|
root_entry->create_timestamp_ = root_creation_timestamp;
|
||||||
|
root_entry->write_timestamp_ = root_creation_timestamp;
|
||||||
root_entry_ = std::unique_ptr<Entry>(root_entry);
|
root_entry_ = std::unique_ptr<Entry>(root_entry);
|
||||||
|
|
||||||
const uint8_t* buffer = map_ptr + BlockToOffsetEGDF(root_sector);
|
// Traverse all child entries
|
||||||
return ReadEntryEGDF(buffer, 0, root_entry) ? Error::kSuccess
|
return ReadEntrySVOD(root_block, 0, root_entry);
|
||||||
: Error::kErrorDamagedFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StfsContainerDevice::ReadEntryEGDF(const uint8_t* buffer,
|
StfsContainerDevice::Error StfsContainerDevice::ReadEntrySVOD(
|
||||||
uint16_t entry_ordinal,
|
uint32_t block, uint32_t ordinal, StfsContainerEntry* parent) {
|
||||||
StfsContainerEntry* parent) {
|
// For games with a large amount of files, the ordinal offset can overrun
|
||||||
const uint8_t* p = buffer + (entry_ordinal * 4);
|
// the current block and potentially hit a hash block.
|
||||||
|
size_t ordinal_offset = ordinal * 0x4;
|
||||||
|
size_t block_offset = ordinal_offset / 0x800;
|
||||||
|
size_t true_ordinal_offset = ordinal_offset % 0x800;
|
||||||
|
|
||||||
uint16_t node_l = xe::load<uint16_t>(p + 0);
|
// Calculate the file & address of the block
|
||||||
uint16_t node_r = xe::load<uint16_t>(p + 2);
|
size_t entry_address, entry_file;
|
||||||
uint32_t sector = xe::load<uint32_t>(p + 4);
|
BlockToOffsetSVOD(block + block_offset, &entry_address, &entry_file);
|
||||||
uint32_t length = xe::load<uint32_t>(p + 8);
|
entry_address += true_ordinal_offset;
|
||||||
uint8_t attributes = xe::load<uint8_t>(p + 12);
|
|
||||||
uint8_t name_length = xe::load<uint8_t>(p + 13);
|
|
||||||
auto name = reinterpret_cast<const char*>(p + 14);
|
|
||||||
|
|
||||||
if (node_l && !ReadEntryEGDF(buffer, node_l, parent)) {
|
// Read block's descriptor
|
||||||
return false;
|
auto data = mmap_.at(entry_file)->data() + entry_address;
|
||||||
|
|
||||||
|
uint16_t node_l = xe::load<uint16_t>(data + 0x00);
|
||||||
|
uint16_t node_r = xe::load<uint16_t>(data + 0x02);
|
||||||
|
uint32_t data_block = xe::load<uint32_t>(data + 0x04);
|
||||||
|
uint32_t length = xe::load<uint32_t>(data + 0x08);
|
||||||
|
uint8_t attributes = xe::load<uint8_t>(data + 0x0C);
|
||||||
|
uint8_t name_length = xe::load<uint8_t>(data + 0x0D);
|
||||||
|
auto name = reinterpret_cast<const char*>(data + 0x0E);
|
||||||
|
auto name_str = std::string(name, name_length);
|
||||||
|
|
||||||
|
// Read the left node
|
||||||
|
if (node_l) {
|
||||||
|
auto node_result = ReadEntrySVOD(block, node_l, parent);
|
||||||
|
if (node_result != Error::kSuccess) {
|
||||||
|
return node_result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto entry = StfsContainerEntry::Create(
|
// Read file & address of block's data
|
||||||
this, parent, std::string(name, name_length), mmap_.get());
|
size_t data_address, data_file;
|
||||||
|
BlockToOffsetSVOD(data_block, &data_address, &data_file);
|
||||||
|
|
||||||
|
// Create the entry
|
||||||
|
// NOTE: SVOD entries don't have timestamps for individual files, which can
|
||||||
|
// cause issues when decrypting games. Using the root entry's timestamp
|
||||||
|
// solves this issues.
|
||||||
|
auto entry = StfsContainerEntry::Create(this, parent, name_str, &mmap_);
|
||||||
if (attributes & kFileAttributeDirectory) {
|
if (attributes & kFileAttributeDirectory) {
|
||||||
// Folder.
|
// Entry is a directory
|
||||||
entry->attributes_ = kFileAttributeDirectory | kFileAttributeReadOnly;
|
entry->attributes_ = kFileAttributeDirectory | kFileAttributeReadOnly;
|
||||||
entry->data_offset_ = 0;
|
entry->data_offset_ = 0;
|
||||||
entry->data_size_ = 0;
|
entry->data_size_ = 0;
|
||||||
|
entry->block_ = block;
|
||||||
|
entry->access_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
entry->create_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
entry->write_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
|
||||||
if (length) {
|
if (length) {
|
||||||
// Not a leaf - read in children.
|
// If length is greater than 0, traverse the directory's children
|
||||||
uint8_t* folder_ptr = mmap_->data() + BlockToOffsetEGDF(sector);
|
auto directory_result = ReadEntrySVOD(data_block, 0, entry.get());
|
||||||
if (!ReadEntryEGDF(folder_ptr, 0, entry.get())) {
|
if (directory_result != Error::kSuccess) {
|
||||||
return false;
|
return directory_result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular file.
|
// Entry is a file
|
||||||
entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
|
entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
|
||||||
entry->size_ = length;
|
entry->size_ = length;
|
||||||
entry->allocation_size_ = xe::round_up(length, bytes_per_sector());
|
entry->allocation_size_ = xe::round_up(length, bytes_per_sector());
|
||||||
entry->data_offset_ = BlockToOffsetEGDF(sector);
|
entry->data_offset_ = data_address;
|
||||||
entry->data_size_ = length;
|
entry->data_size_ = length;
|
||||||
|
entry->block_ = data_block;
|
||||||
|
entry->access_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
entry->create_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
entry->write_timestamp_ = root_entry_->create_timestamp();
|
||||||
|
|
||||||
// Fill in all block records, sector by sector.
|
// Fill in all block records, sector by sector.
|
||||||
if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
|
if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
|
||||||
uint32_t sector_index = sector;
|
uint32_t block_index = data_block;
|
||||||
size_t remaining_size = xe::round_up(length, 0x800);
|
size_t remaining_size = xe::round_up(length, 0x800);
|
||||||
|
|
||||||
size_t last_record = -1;
|
size_t last_record = -1;
|
||||||
size_t last_offset = -1;
|
size_t last_offset = -1;
|
||||||
while (remaining_size) {
|
while (remaining_size) {
|
||||||
size_t block_size = 0x800;
|
const size_t BLOCK_SIZE = 0x800;
|
||||||
size_t offset = BlockToOffsetEGDF(sector_index);
|
|
||||||
sector_index++;
|
size_t offset, file_index;
|
||||||
remaining_size -= block_size;
|
BlockToOffsetSVOD(block_index, &offset, &file_index);
|
||||||
|
|
||||||
|
block_index++;
|
||||||
|
remaining_size -= BLOCK_SIZE;
|
||||||
|
|
||||||
if (offset - last_offset == 0x800) {
|
if (offset - last_offset == 0x800) {
|
||||||
// Consecutive, so append to last entry.
|
// Consecutive, so append to last entry.
|
||||||
entry->block_list_[last_record].length += block_size;
|
entry->block_list_[last_record].length += BLOCK_SIZE;
|
||||||
last_offset = offset;
|
last_offset = offset;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->block_list_.push_back({offset, block_size});
|
entry->block_list_.push_back({file_index, offset, BLOCK_SIZE});
|
||||||
last_record = entry->block_list_.size() - 1;
|
last_record = entry->block_list_.size() - 1;
|
||||||
last_offset = offset;
|
last_offset = offset;
|
||||||
}
|
}
|
||||||
|
@ -285,17 +389,82 @@ bool StfsContainerDevice::ReadEntryEGDF(const uint8_t* buffer,
|
||||||
|
|
||||||
parent->children_.emplace_back(std::move(entry));
|
parent->children_.emplace_back(std::move(entry));
|
||||||
|
|
||||||
// Read next file in the list.
|
// Read the right node.
|
||||||
if (node_r && !ReadEntryEGDF(buffer, node_r, parent)) {
|
if (node_r) {
|
||||||
return false;
|
auto node_result = ReadEntrySVOD(block, node_r, parent);
|
||||||
|
if (node_result != Error::kSuccess) {
|
||||||
|
return node_result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return Error::kSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesSTFS(
|
void StfsContainerDevice::BlockToOffsetSVOD(size_t block, size_t* out_address,
|
||||||
const uint8_t* map_ptr) {
|
size_t* out_file_index) {
|
||||||
auto root_entry = new StfsContainerEntry(this, nullptr, "", mmap_.get());
|
// SVOD Systems use hash blocks for integrity checks. These hash blocks
|
||||||
|
// cause blocks to be discontinuous in memory, and must be accounted for.
|
||||||
|
// - Each data block is 0x800 bytes in length
|
||||||
|
// - Every group of 0x198 data blocks is preceded a Level0 hash table.
|
||||||
|
// Level0 tables contain 0xCC hashes, each representing two data blocks.
|
||||||
|
// The total size of each Level0 hash table is 0x1000 bytes in length.
|
||||||
|
// - Every 0xA1C4 Level0 hash tables is preceded by a Level1 hash table.
|
||||||
|
// Level1 tables contain 0xCB hashes, each representing two Level0 hashes.
|
||||||
|
// The total size of each Level1 hash table is 0x1000 bytes in length.
|
||||||
|
// - Files are split into fragments of 0xA290000 bytes in length,
|
||||||
|
// consisting of 0x14388 data blocks, 0xCB Level0 hash tables, and 0x1
|
||||||
|
// Level1 hash table.
|
||||||
|
|
||||||
|
const size_t BLOCK_SIZE = 0x800;
|
||||||
|
const size_t HASH_BLOCK_SIZE = 0x1000;
|
||||||
|
const size_t BLOCKS_PER_L0_HASH = 0x198;
|
||||||
|
const size_t HASHES_PER_L1_HASH = 0xA1C4;
|
||||||
|
const size_t BLOCKS_PER_FILE = 0x14388;
|
||||||
|
const size_t MAX_FILE_SIZE = 0xA290000;
|
||||||
|
const size_t BLOCK_OFFSET = header_.svod_volume_descriptor.data_block_offset;
|
||||||
|
const SvodLayoutType LAYOUT = header_.svod_volume_descriptor.layout_type;
|
||||||
|
|
||||||
|
// Resolve the true block address and file index
|
||||||
|
size_t true_block = block - (BLOCK_OFFSET * 2);
|
||||||
|
if (LAYOUT == kEnhancedGDFLayout) {
|
||||||
|
// EGDF has an 0x1000 byte offset, which is two blocks
|
||||||
|
true_block += 0x2;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t file_block = true_block % BLOCKS_PER_FILE;
|
||||||
|
size_t file_index = true_block / BLOCKS_PER_FILE;
|
||||||
|
size_t offset = 0;
|
||||||
|
|
||||||
|
// Calculate offset caused by Level0 Hash Tables
|
||||||
|
size_t level0_table_count = (file_block / BLOCKS_PER_L0_HASH) + 1;
|
||||||
|
offset += level0_table_count * HASH_BLOCK_SIZE;
|
||||||
|
|
||||||
|
// Calculate offset caused by Level1 Hash Tables
|
||||||
|
size_t level1_table_count = (level0_table_count / HASHES_PER_L1_HASH) + 1;
|
||||||
|
offset += level1_table_count * HASH_BLOCK_SIZE;
|
||||||
|
|
||||||
|
// For single-file SVOD layouts, include the size of the header in the offset.
|
||||||
|
if (LAYOUT == kSingleFileLayout) {
|
||||||
|
offset += base_offset_;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t block_address = (file_block * BLOCK_SIZE) + offset;
|
||||||
|
|
||||||
|
// If the offset causes the block address to overrun the file, round it.
|
||||||
|
if (block_address >= MAX_FILE_SIZE) {
|
||||||
|
file_index += 1;
|
||||||
|
block_address %= MAX_FILE_SIZE;
|
||||||
|
block_address += 0x2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_address = block_address;
|
||||||
|
*out_file_index = file_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
|
||||||
|
auto data = mmap_.at(0)->data();
|
||||||
|
|
||||||
|
auto root_entry = new StfsContainerEntry(this, nullptr, "", &mmap_);
|
||||||
root_entry->attributes_ = kFileAttributeDirectory;
|
root_entry->attributes_ = kFileAttributeDirectory;
|
||||||
root_entry_ = std::unique_ptr<Entry>(root_entry);
|
root_entry_ = std::unique_ptr<Entry>(root_entry);
|
||||||
|
|
||||||
|
@ -305,7 +474,7 @@ StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesSTFS(
|
||||||
auto& volume_descriptor = header_.stfs_volume_descriptor;
|
auto& volume_descriptor = header_.stfs_volume_descriptor;
|
||||||
uint32_t table_block_index = volume_descriptor.file_table_block_number;
|
uint32_t table_block_index = volume_descriptor.file_table_block_number;
|
||||||
for (size_t n = 0; n < volume_descriptor.file_table_block_count; n++) {
|
for (size_t n = 0; n < volume_descriptor.file_table_block_count; n++) {
|
||||||
const uint8_t* p = map_ptr + BlockToOffsetSTFS(table_block_index);
|
const uint8_t* p = data + BlockToOffsetSTFS(table_block_index);
|
||||||
for (size_t m = 0; m < 0x1000 / 0x40; m++) {
|
for (size_t m = 0; m < 0x1000 / 0x40; m++) {
|
||||||
const uint8_t* filename = p; // 0x28b
|
const uint8_t* filename = p; // 0x28b
|
||||||
if (filename[0] == 0) {
|
if (filename[0] == 0) {
|
||||||
|
@ -333,11 +502,11 @@ StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesSTFS(
|
||||||
parent_entry = all_entries[path_indicator];
|
parent_entry = all_entries[path_indicator];
|
||||||
}
|
}
|
||||||
|
|
||||||
auto entry = StfsContainerEntry::Create(
|
std::string name_str(reinterpret_cast<const char*>(filename),
|
||||||
this, parent_entry,
|
filename_length_flags & 0x3F);
|
||||||
std::string(reinterpret_cast<const char*>(filename),
|
auto entry =
|
||||||
filename_length_flags & 0x3F),
|
StfsContainerEntry::Create(this, parent_entry, name_str, &mmap_);
|
||||||
mmap_.get());
|
|
||||||
// bit 0x40 = consecutive blocks (not fragmented?)
|
// bit 0x40 = consecutive blocks (not fragmented?)
|
||||||
if (filename_length_flags & 0x80) {
|
if (filename_length_flags & 0x80) {
|
||||||
entry->attributes_ = kFileAttributeDirectory;
|
entry->attributes_ = kFileAttributeDirectory;
|
||||||
|
@ -367,11 +536,11 @@ StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesSTFS(
|
||||||
size_t block_size =
|
size_t block_size =
|
||||||
std::min(static_cast<size_t>(0x1000), remaining_size);
|
std::min(static_cast<size_t>(0x1000), remaining_size);
|
||||||
size_t offset = BlockToOffsetSTFS(block_index);
|
size_t offset = BlockToOffsetSTFS(block_index);
|
||||||
entry->block_list_.push_back({offset, block_size});
|
entry->block_list_.push_back({0, offset, block_size});
|
||||||
remaining_size -= block_size;
|
remaining_size -= block_size;
|
||||||
auto block_hash = GetBlockHash(map_ptr, block_index, 0);
|
auto block_hash = GetBlockHash(data, block_index, 0);
|
||||||
if (table_size_shift_ && block_hash.info < 0x80) {
|
if (table_size_shift_ && block_hash.info < 0x80) {
|
||||||
block_hash = GetBlockHash(map_ptr, block_index, 1);
|
block_hash = GetBlockHash(data, block_index, 1);
|
||||||
}
|
}
|
||||||
block_index = block_hash.next_block_index;
|
block_index = block_hash.next_block_index;
|
||||||
info = block_hash.info;
|
info = block_hash.info;
|
||||||
|
@ -381,9 +550,9 @@ StfsContainerDevice::Error StfsContainerDevice::ReadAllEntriesSTFS(
|
||||||
parent_entry->children_.emplace_back(std::move(entry));
|
parent_entry->children_.emplace_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto block_hash = GetBlockHash(map_ptr, table_block_index, 0);
|
auto block_hash = GetBlockHash(data, table_block_index, 0);
|
||||||
if (table_size_shift_ && block_hash.info < 0x80) {
|
if (table_size_shift_ && block_hash.info < 0x80) {
|
||||||
block_hash = GetBlockHash(map_ptr, table_block_index, 1);
|
block_hash = GetBlockHash(data, table_block_index, 1);
|
||||||
}
|
}
|
||||||
table_block_index = block_hash.next_block_index;
|
table_block_index = block_hash.next_block_index;
|
||||||
if (table_block_index == 0xFFFFFF) {
|
if (table_block_index == 0xFFFFFF) {
|
||||||
|
@ -402,41 +571,25 @@ size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) {
|
||||||
block_shift = package_type_ == StfsPackageType::kCon ? 1 : 0;
|
block_shift = package_type_ == StfsPackageType::kCon ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header_.descriptor_type == StfsDescriptorType::kStfs) {
|
// For every level there is a hash table
|
||||||
// For every level there is a hash table
|
// Level 0: hash table of next 170 blocks
|
||||||
// Level 0: hash table of next 170 blocks
|
// Level 1: hash table of next 170 hash tables
|
||||||
// Level 1: hash table of next 170 hash tables
|
// Level 2: hash table of next 170 level 1 hash tables
|
||||||
// Level 2: hash table of next 170 level 1 hash tables
|
// And so on...
|
||||||
// And so on...
|
uint64_t base = kSTFSHashSpacing;
|
||||||
uint64_t base = kSTFSHashSpacing;
|
block = block_index;
|
||||||
block = block_index;
|
for (uint32_t i = 0; i < 3; i++) {
|
||||||
for (uint32_t i = 0; i < 3; i++) {
|
block += (block_index + (base << block_shift)) / (base << block_shift);
|
||||||
block += (block_index + (base << block_shift)) / (base << block_shift);
|
if (block_index < base) {
|
||||||
if (block_index < base) {
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
base *= kSTFSHashSpacing;
|
|
||||||
}
|
}
|
||||||
} else if (header_.descriptor_type == StfsDescriptorType::kSvod) {
|
|
||||||
// Level 0: Hashes for the next 204 blocks
|
base *= kSTFSHashSpacing;
|
||||||
// Level 1: Hashes for the next 203 hash blocks + 1 for the next level 0
|
|
||||||
// 10......[204 blocks].....0.....[204 blocks].....0
|
|
||||||
// There are 0xA1C4 (41412) blocks for every level 1 hash table.
|
|
||||||
block = block_index;
|
|
||||||
block += (block_index + 204) / 204; // Level 0
|
|
||||||
block += (block_index + 204 * 203) / (204 * 203); // Level 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return xe::round_up(header_.header_size, 0x1000) + (block << 12);
|
return xe::round_up(header_.header_size, 0x1000) + (block << 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t StfsContainerDevice::BlockToOffsetEGDF(uint64_t sector) {
|
|
||||||
size_t offset = BlockToOffsetSTFS(
|
|
||||||
(sector / 2) - header_.svod_volume_descriptor.data_block_offset + 1);
|
|
||||||
return offset + ((sector & 0x1) << 11); // Sectors are 0x800 bytes.
|
|
||||||
}
|
|
||||||
|
|
||||||
StfsContainerDevice::BlockHash StfsContainerDevice::GetBlockHash(
|
StfsContainerDevice::BlockHash StfsContainerDevice::GetBlockHash(
|
||||||
const uint8_t* map_ptr, uint32_t block_index, uint32_t table_offset) {
|
const uint8_t* map_ptr, uint32_t block_index, uint32_t table_offset) {
|
||||||
uint32_t record = block_index % 0xAA;
|
uint32_t record = block_index % 0xAA;
|
||||||
|
@ -496,11 +649,6 @@ bool StfsHeader::Read(const uint8_t* p) {
|
||||||
header_size = xe::load_and_swap<uint32_t>(p + 0x340);
|
header_size = xe::load_and_swap<uint32_t>(p + 0x340);
|
||||||
content_type = (StfsContentType)xe::load_and_swap<uint32_t>(p + 0x344);
|
content_type = (StfsContentType)xe::load_and_swap<uint32_t>(p + 0x344);
|
||||||
metadata_version = xe::load_and_swap<uint32_t>(p + 0x348);
|
metadata_version = xe::load_and_swap<uint32_t>(p + 0x348);
|
||||||
if (metadata_version > 1) {
|
|
||||||
// This is a variant of thumbnail data/etc.
|
|
||||||
// Can just ignore it for now (until we parse thumbnails).
|
|
||||||
XELOGW("STFSContainer doesn't support version %d yet", metadata_version);
|
|
||||||
}
|
|
||||||
content_size = xe::load_and_swap<uint32_t>(p + 0x34C);
|
content_size = xe::load_and_swap<uint32_t>(p + 0x34C);
|
||||||
media_id = xe::load_and_swap<uint32_t>(p + 0x354);
|
media_id = xe::load_and_swap<uint32_t>(p + 0x354);
|
||||||
version = xe::load_and_swap<uint32_t>(p + 0x358);
|
version = xe::load_and_swap<uint32_t>(p + 0x358);
|
||||||
|
@ -541,8 +689,70 @@ bool StfsHeader::Read(const uint8_t* p) {
|
||||||
title_thumbnail_image_size = xe::load_and_swap<uint32_t>(p + 0x1716);
|
title_thumbnail_image_size = xe::load_and_swap<uint32_t>(p + 0x1716);
|
||||||
std::memcpy(thumbnail_image, p + 0x171A, 0x4000);
|
std::memcpy(thumbnail_image, p + 0x171A, 0x4000);
|
||||||
std::memcpy(title_thumbnail_image, p + 0x571A, 0x4000);
|
std::memcpy(title_thumbnail_image, p + 0x571A, 0x4000);
|
||||||
|
|
||||||
|
// Metadata v2 Fields
|
||||||
|
if (metadata_version == 2) {
|
||||||
|
std::memcpy(series_id, p + 0x3B1, 0x10);
|
||||||
|
std::memcpy(season_id, p + 0x3C1, 0x10);
|
||||||
|
season_number = xe::load_and_swap<uint16_t>(p + 0x3D1);
|
||||||
|
episode_number = xe::load_and_swap<uint16_t>(p + 0x3D5);
|
||||||
|
|
||||||
|
for (size_t n = 0; n < 0x300 / 2; n++) {
|
||||||
|
additonal_display_names[n] =
|
||||||
|
xe::load_and_swap<uint16_t>(p + 0x541A + n * 2);
|
||||||
|
additional_display_descriptions[n] =
|
||||||
|
xe::load_and_swap<uint16_t>(p + 0x941A + n * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* StfsContainerDevice::ReadMagic(const std::wstring& path) {
|
||||||
|
auto map = MappedMemory::Open(path, MappedMemory::Mode::kRead, 0, 4);
|
||||||
|
auto magic_data = xe::load<uint32_t>(map->data());
|
||||||
|
auto magic_bytes = static_cast<char*>(static_cast<void*>(&magic_data));
|
||||||
|
return std::move(magic_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StfsContainerDevice::ResolveFromFolder(const std::wstring& path) {
|
||||||
|
// Scan through folders until a file with magic is found
|
||||||
|
std::queue<filesystem::FileInfo> queue;
|
||||||
|
|
||||||
|
filesystem::FileInfo folder;
|
||||||
|
filesystem::GetInfo(local_path_, &folder);
|
||||||
|
queue.push(folder);
|
||||||
|
|
||||||
|
while (!queue.empty()) {
|
||||||
|
auto current_file = queue.front();
|
||||||
|
queue.pop();
|
||||||
|
|
||||||
|
if (current_file.type == filesystem::FileInfo::Type::kDirectory) {
|
||||||
|
auto path = xe::join_paths(current_file.path, current_file.name);
|
||||||
|
auto child_files = filesystem::ListFiles(path);
|
||||||
|
for (auto file : child_files) {
|
||||||
|
queue.push(file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to read the file's magic
|
||||||
|
auto path = xe::join_paths(current_file.path, current_file.name);
|
||||||
|
auto magic = ReadMagic(path);
|
||||||
|
|
||||||
|
if (memcmp(magic, "LIVE", 4) == 0 || memcmp(magic, "PIRS", 4) == 0 ||
|
||||||
|
memcmp(magic, "CON ", 4) == 0) {
|
||||||
|
local_path_ = xe::join_paths(current_file.path, current_file.name);
|
||||||
|
XELOGI("STFS Package found: %s", xe::to_string(local_path_).c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local_path_ == path) {
|
||||||
|
// Could not find a suitable container file
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace vfs
|
} // namespace vfs
|
||||||
} // namespace xe
|
} // namespace xe
|
|
@ -10,6 +10,7 @@
|
||||||
#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
|
#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
|
||||||
#define XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
|
#define XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
@ -91,6 +92,13 @@ enum SvodDeviceFeatures {
|
||||||
kFeatureHasEnhancedGDFLayout = 0x40,
|
kFeatureHasEnhancedGDFLayout = 0x40,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum SvodLayoutType {
|
||||||
|
kUnknownLayout = 0x0,
|
||||||
|
kEnhancedGDFLayout = 0x1,
|
||||||
|
kXSFLayout = 0x2,
|
||||||
|
kSingleFileLayout = 0x4,
|
||||||
|
};
|
||||||
|
|
||||||
struct SvodVolumeDescriptor {
|
struct SvodVolumeDescriptor {
|
||||||
bool Read(const uint8_t* p);
|
bool Read(const uint8_t* p);
|
||||||
|
|
||||||
|
@ -103,6 +111,8 @@ struct SvodVolumeDescriptor {
|
||||||
uint32_t data_block_count;
|
uint32_t data_block_count;
|
||||||
uint32_t data_block_offset;
|
uint32_t data_block_offset;
|
||||||
// 0x5 padding bytes...
|
// 0x5 padding bytes...
|
||||||
|
|
||||||
|
SvodLayoutType layout_type;
|
||||||
};
|
};
|
||||||
|
|
||||||
class StfsHeader {
|
class StfsHeader {
|
||||||
|
@ -143,6 +153,14 @@ class StfsHeader {
|
||||||
uint32_t title_thumbnail_image_size;
|
uint32_t title_thumbnail_image_size;
|
||||||
uint8_t thumbnail_image[0x4000];
|
uint8_t thumbnail_image[0x4000];
|
||||||
uint8_t title_thumbnail_image[0x4000];
|
uint8_t title_thumbnail_image[0x4000];
|
||||||
|
|
||||||
|
// Metadata v2 Fields
|
||||||
|
uint8_t series_id[0x10];
|
||||||
|
uint8_t season_id[0x10];
|
||||||
|
int16_t season_number;
|
||||||
|
int16_t episode_number;
|
||||||
|
wchar_t additonal_display_names[0x300 / 2];
|
||||||
|
wchar_t additional_display_descriptions[0x300 / 2];
|
||||||
};
|
};
|
||||||
|
|
||||||
class StfsContainerDevice : public Device {
|
class StfsContainerDevice : public Device {
|
||||||
|
@ -156,7 +174,7 @@ class StfsContainerDevice : public Device {
|
||||||
Entry* ResolvePath(std::string path) override;
|
Entry* ResolvePath(std::string path) override;
|
||||||
|
|
||||||
uint32_t total_allocation_units() const override {
|
uint32_t total_allocation_units() const override {
|
||||||
return uint32_t(mmap_->size() / sectors_per_allocation_unit() /
|
return uint32_t(mmap_total_size_ / sectors_per_allocation_unit() /
|
||||||
bytes_per_sector());
|
bytes_per_sector());
|
||||||
}
|
}
|
||||||
uint32_t available_allocation_units() const override { return 0; }
|
uint32_t available_allocation_units() const override { return 0; }
|
||||||
|
@ -178,23 +196,30 @@ class StfsContainerDevice : public Device {
|
||||||
};
|
};
|
||||||
|
|
||||||
const uint32_t kSTFSHashSpacing = 170;
|
const uint32_t kSTFSHashSpacing = 170;
|
||||||
const uint32_t kSVODHashSpacing = 204;
|
|
||||||
|
|
||||||
|
const char* ReadMagic(const std::wstring& path);
|
||||||
|
bool ResolveFromFolder(const std::wstring& path);
|
||||||
|
|
||||||
|
Error MapFiles();
|
||||||
Error ReadHeaderAndVerify(const uint8_t* map_ptr);
|
Error ReadHeaderAndVerify(const uint8_t* map_ptr);
|
||||||
Error ReadAllEntriesEGDF(const uint8_t* map_ptr);
|
|
||||||
bool ReadEntryEGDF(const uint8_t* buffer, uint16_t entry_ordinal,
|
|
||||||
StfsContainerEntry* parent);
|
|
||||||
|
|
||||||
Error ReadAllEntriesSTFS(const uint8_t* map_ptr);
|
Error ReadSVOD();
|
||||||
|
Error ReadEntrySVOD(uint32_t sector, uint32_t ordinal,
|
||||||
|
StfsContainerEntry* parent);
|
||||||
|
void BlockToOffsetSVOD(size_t sector, size_t* address, size_t* file_index);
|
||||||
|
|
||||||
|
Error ReadSTFS();
|
||||||
size_t BlockToOffsetSTFS(uint64_t block);
|
size_t BlockToOffsetSTFS(uint64_t block);
|
||||||
size_t BlockToOffsetEGDF(uint64_t block);
|
|
||||||
|
|
||||||
BlockHash GetBlockHash(const uint8_t* map_ptr, uint32_t block_index,
|
BlockHash GetBlockHash(const uint8_t* map_ptr, uint32_t block_index,
|
||||||
uint32_t table_offset);
|
uint32_t table_offset);
|
||||||
|
|
||||||
std::wstring local_path_;
|
std::wstring local_path_;
|
||||||
std::unique_ptr<MappedMemory> mmap_;
|
std::map<size_t, std::unique_ptr<MappedMemory>> mmap_;
|
||||||
|
size_t mmap_total_size_;
|
||||||
|
|
||||||
|
size_t base_offset_;
|
||||||
|
size_t magic_offset_;
|
||||||
std::unique_ptr<Entry> root_entry_;
|
std::unique_ptr<Entry> root_entry_;
|
||||||
StfsPackageType package_type_;
|
StfsPackageType package_type_;
|
||||||
StfsHeader header_;
|
StfsHeader header_;
|
||||||
|
|
|
@ -8,15 +8,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "xenia/vfs/devices/stfs_container_entry.h"
|
#include "xenia/vfs/devices/stfs_container_entry.h"
|
||||||
|
|
||||||
#include "xenia/base/math.h"
|
#include "xenia/base/math.h"
|
||||||
#include "xenia/vfs/devices/stfs_container_file.h"
|
#include "xenia/vfs/devices/stfs_container_file.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace vfs {
|
namespace vfs {
|
||||||
|
|
||||||
StfsContainerEntry::StfsContainerEntry(Device* device, Entry* parent,
|
StfsContainerEntry::StfsContainerEntry(Device* device, Entry* parent,
|
||||||
std::string path, MappedMemory* mmap)
|
std::string path,
|
||||||
|
MultifileMemoryMap* mmap)
|
||||||
: Entry(device, parent, path),
|
: Entry(device, parent, path),
|
||||||
mmap_(mmap),
|
mmap_(mmap),
|
||||||
data_offset_(0),
|
data_offset_(0),
|
||||||
|
@ -25,7 +27,7 @@ StfsContainerEntry::StfsContainerEntry(Device* device, Entry* parent,
|
||||||
StfsContainerEntry::~StfsContainerEntry() = default;
|
StfsContainerEntry::~StfsContainerEntry() = default;
|
||||||
|
|
||||||
std::unique_ptr<StfsContainerEntry> StfsContainerEntry::Create(
|
std::unique_ptr<StfsContainerEntry> StfsContainerEntry::Create(
|
||||||
Device* device, Entry* parent, std::string name, MappedMemory* mmap) {
|
Device* device, Entry* parent, std::string name, MultifileMemoryMap* mmap) {
|
||||||
auto path = xe::join_paths(parent->path(), name);
|
auto path = xe::join_paths(parent->path(), name);
|
||||||
auto entry = std::make_unique<StfsContainerEntry>(device, parent, path, mmap);
|
auto entry = std::make_unique<StfsContainerEntry>(device, parent, path, mmap);
|
||||||
|
|
||||||
|
@ -38,4 +40,4 @@ X_STATUS StfsContainerEntry::Open(uint32_t desired_access, File** out_file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace vfs
|
} // namespace vfs
|
||||||
} // namespace xe
|
} // namespace xe
|
|
@ -10,6 +10,7 @@
|
||||||
#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
||||||
#define XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
#define XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
@ -20,27 +21,30 @@
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace vfs {
|
namespace vfs {
|
||||||
|
typedef std::map<size_t, std::unique_ptr<MappedMemory>> MultifileMemoryMap;
|
||||||
|
|
||||||
class StfsContainerDevice;
|
class StfsContainerDevice;
|
||||||
|
|
||||||
class StfsContainerEntry : public Entry {
|
class StfsContainerEntry : public Entry {
|
||||||
public:
|
public:
|
||||||
StfsContainerEntry(Device* device, Entry* parent, std::string path,
|
StfsContainerEntry(Device* device, Entry* parent, std::string path,
|
||||||
MappedMemory* mmap);
|
MultifileMemoryMap* mmap);
|
||||||
~StfsContainerEntry() override;
|
~StfsContainerEntry() override;
|
||||||
|
|
||||||
static std::unique_ptr<StfsContainerEntry> Create(Device* device,
|
static std::unique_ptr<StfsContainerEntry> Create(Device* device,
|
||||||
Entry* parent,
|
Entry* parent,
|
||||||
std::string name,
|
std::string name,
|
||||||
MappedMemory* mmap);
|
MultifileMemoryMap* mmap);
|
||||||
|
|
||||||
MappedMemory* mmap() const { return mmap_; }
|
MultifileMemoryMap* mmap() const { return mmap_; }
|
||||||
size_t data_offset() const { return data_offset_; }
|
size_t data_offset() const { return data_offset_; }
|
||||||
size_t data_size() const { return data_size_; }
|
size_t data_size() const { return data_size_; }
|
||||||
|
size_t block() const { return block_; }
|
||||||
|
|
||||||
X_STATUS Open(uint32_t desired_access, File** out_file) override;
|
X_STATUS Open(uint32_t desired_access, File** out_file) override;
|
||||||
|
|
||||||
struct BlockRecord {
|
struct BlockRecord {
|
||||||
|
size_t file;
|
||||||
size_t offset;
|
size_t offset;
|
||||||
size_t length;
|
size_t length;
|
||||||
};
|
};
|
||||||
|
@ -49,13 +53,14 @@ class StfsContainerEntry : public Entry {
|
||||||
private:
|
private:
|
||||||
friend class StfsContainerDevice;
|
friend class StfsContainerDevice;
|
||||||
|
|
||||||
MappedMemory* mmap_;
|
MultifileMemoryMap* mmap_;
|
||||||
size_t data_offset_;
|
size_t data_offset_;
|
||||||
size_t data_size_;
|
size_t data_size_;
|
||||||
|
size_t block_;
|
||||||
std::vector<BlockRecord> block_list_;
|
std::vector<BlockRecord> block_list_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace vfs
|
} // namespace vfs
|
||||||
} // namespace xe
|
} // namespace xe
|
||||||
|
|
||||||
#endif // XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
#endif // XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
|
|
@ -34,7 +34,6 @@ X_STATUS StfsContainerFile::ReadSync(void* buffer, size_t buffer_length,
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t src_offset = 0;
|
size_t src_offset = 0;
|
||||||
uint8_t* src = entry_->mmap()->data();
|
|
||||||
uint8_t* p = reinterpret_cast<uint8_t*>(buffer);
|
uint8_t* p = reinterpret_cast<uint8_t*>(buffer);
|
||||||
size_t remaining_length =
|
size_t remaining_length =
|
||||||
std::min(buffer_length, entry_->size() - byte_offset);
|
std::min(buffer_length, entry_->size() - byte_offset);
|
||||||
|
@ -48,6 +47,8 @@ X_STATUS StfsContainerFile::ReadSync(void* buffer, size_t buffer_length,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t* src = entry_->mmap()->at(record.file)->data();
|
||||||
|
|
||||||
size_t read_offset =
|
size_t read_offset =
|
||||||
(byte_offset > src_offset) ? byte_offset - src_offset : 0;
|
(byte_offset > src_offset) ? byte_offset - src_offset : 0;
|
||||||
size_t read_length =
|
size_t read_length =
|
||||||
|
@ -66,4 +67,4 @@ X_STATUS StfsContainerFile::ReadSync(void* buffer, size_t buffer_length,
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace vfs
|
} // namespace vfs
|
||||||
} // namespace xe
|
} // namespace xe
|
|
@ -1 +1 @@
|
||||||
Subproject commit a0ada2e935324db5c951571c52c9b07307ea422f
|
Subproject commit 6860c8def0ba7559bf077515b7a7ff63ad3444f8
|
Loading…
Reference in New Issue