handle missing block indexes in qst decoder

This commit is contained in:
Martin Michelsen
2026-05-29 06:56:56 -07:00
parent bb0ead8650
commit c09ee2da85
3 changed files with 58 additions and 31 deletions
+7 -4
View File
@@ -1564,10 +1564,13 @@ Action a_decode_qst(
Action a_encode_qst(
"encode-qst", "\
encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\
Encode the input quest file (in .bin/.dat format) into a .qst file. If\n\
--download is given, generates a download .qst instead of an online .qst.\n\
Specify the quest\'s game version with one of the --dc-nte, --dc-v1,\n\
--dc-v2, --pc, --gc-nte, --gc, --gc-ep3, --xb, or --bb options.\n",
Encode the input quest files (in .bin format) into a .qst file. There must\n\
be a .dat file with the same name as the .bin file, which will be included\n\
in the resulting .qst file. If there is a .pvr file with the same name as\n\
the .bin file, it will be included as well. If --download is given,\n\
generates a download .qst instead of an online .qst. Specify the quest\'s\n\
game version with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte,\n\
--gc, --gc-ep3, --xb, or --bb options.\n",
+[](phosg::Arguments& args) {
std::string input_filename = args.get<std::string>(1, false);
if (input_filename.empty() || (input_filename == "-")) {
+50 -26
View File
@@ -1064,9 +1064,19 @@ template <typename HeaderT, typename OpenFileT>
static std::unordered_map<std::string, std::string> decode_qst_data_t(const std::string& data) {
phosg::StringReader r(data);
std::unordered_map<std::string, std::string> files;
std::unordered_map<std::string, size_t> file_remaining_bytes;
struct File {
struct Block {
uint32_t index = 0;
const void* data = nullptr;
size_t size = 0;
};
std::deque<Block> blocks;
size_t total_size = 0;
};
std::unordered_map<std::string, File> files;
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
bool assemble_in_order = true;
while (!r.eof()) {
// Handle BB's implicit 8-byte command alignment
static constexpr size_t alignment = sizeof(HeaderT);
@@ -1098,11 +1108,7 @@ static std::unordered_map<std::string, std::string> decode_qst_data_t(const std:
}
const auto& cmd = r.get<OpenFileT>();
std::string internal_filename = cmd.filename.decode();
if (!files.emplace(internal_filename, "").second) {
throw std::runtime_error("qst opens the same file multiple times: " + internal_filename);
}
if (!file_remaining_bytes.emplace(internal_filename, cmd.file_size).second) {
if (!files.emplace(internal_filename, File{{}, cmd.file_size}).second) {
throw std::runtime_error("qst opens the same file multiple times: " + internal_filename);
}
@@ -1113,41 +1119,59 @@ static std::unordered_map<std::string, std::string> decode_qst_data_t(const std:
throw std::runtime_error("qst write file command has incorrect size");
}
const auto& cmd = r.get<S_WriteFile_13_A7>();
if (cmd.data_size > 0x400) {
throw std::runtime_error("qst contains invalid write command");
}
std::string filename = cmd.filename.decode();
std::string& file_data = files.at(filename);
size_t& remaining_bytes = file_remaining_bytes.at(filename);
if (file_data.size() & 0x3FF) {
throw std::runtime_error("qst contains uneven chunks out of order");
if (cmd.data_size > 0x400) {
throw std::runtime_error(std::format(
"qst chunk {}:{:02X} has invalid size {:08X}", filename, header.flag, cmd.data_size));
}
if (header.flag != file_data.size() / 0x400) {
throw std::runtime_error("qst contains chunks out of order");
auto& file = files.at(filename);
size_t offset = header.flag * 0x400;
if (offset + cmd.data_size > file.total_size) {
throw std::runtime_error(std::format("qst chunk {}:{:02X} extends beyond end of file", filename, header.flag));
}
auto& block = file.blocks.emplace_back();
block.index = header.flag;
block.data = cmd.data.data();
block.size = cmd.data_size;
if (header.flag) {
assemble_in_order = false;
}
file_data.append(reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
remaining_bytes -= cmd.data_size;
} else {
throw std::runtime_error("invalid command in qst file");
}
}
for (const auto& it : file_remaining_bytes) {
if (it.second) {
throw std::runtime_error(std::format("expected {} (0x{:X}) more bytes for file {}", it.second, it.second, it.first));
// QST is an unfortunately underspecified format, and there are technically many versions of it out there (though I'm
// sure no one has ever thought much about this). Here we handle one of the format's quirks, in which some QST files
// have all zeroes for chunk indexes, so we just assume in that case that the chunks are in the correct order in the
// source file.
std::unordered_map<std::string, std::string> ret;
for (const auto& [name, file] : files) {
std::string& assembled_file = ret.emplace(name, "").first->second;
if (assemble_in_order) {
static_game_data_log.warning_f(
"QST file entry {} is missing block indexes; assuming blocks are assembled in order", name);
for (const auto& block : file.blocks) {
assembled_file.append(reinterpret_cast<const char*>(block.data), block.size);
}
} else {
assembled_file.resize(file.total_size, 0x00);
for (const auto& block : file.blocks) {
memcpy(assembled_file.data() + block.index * 0x400, block.data, block.size);
}
}
}
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
for (auto& it : files) {
it.second = decode_dlq_data(it.second);
for (auto& [_, data] : ret) {
data = decode_dlq_data(data);
}
}
return files;
return ret;
}
std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data) {
+1 -1
View File
@@ -39,7 +39,7 @@ struct QuestMetadata {
std::string str() const;
};
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
uint32_t quest_number = 0;
Episode episode = Episode::NONE;
std::array<FloorAssignment, 0x12> floor_assignments;
bool joinable = false;