You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
11 KiB
381 lines
11 KiB
/** |
|
* @file dc_save_codec.cpp |
|
* @brief Dreamcast save compression with zlib |
|
*/ |
|
|
|
#ifdef __DREAMCAST__ |
|
|
|
#include "dc_save_codec.hpp" |
|
|
|
#include "utils/log.hpp" |
|
|
|
#include <algorithm> |
|
#include <cstdint> |
|
#include <cstdio> |
|
#include <cstring> |
|
#include <limits> |
|
#include <string> |
|
#include <zlib.h> |
|
|
|
#include <dc/fs_vmu.h> |
|
#include <dc/vmu_pkg.h> |
|
#include <kos/fs.h> |
|
|
|
namespace devilution { |
|
namespace dc { |
|
|
|
namespace { |
|
|
|
constexpr size_t MaxSaveDataSize = 512 * 1024; |
|
constexpr size_t SaveHeaderSize = 16; |
|
constexpr uint8_t SaveFormatVersion = 1; |
|
constexpr char SaveMagic[4] = { 'D', 'X', 'Z', '1' }; |
|
|
|
enum class SaveCodec : uint8_t { |
|
Raw = 0, |
|
Zlib = 1, |
|
}; |
|
|
|
uint32_t ReadU32LE(const std::byte *data) |
|
{ |
|
const auto *bytes = reinterpret_cast<const uint8_t *>(data); |
|
return static_cast<uint32_t>(bytes[0]) |
|
| (static_cast<uint32_t>(bytes[1]) << 8) |
|
| (static_cast<uint32_t>(bytes[2]) << 16) |
|
| (static_cast<uint32_t>(bytes[3]) << 24); |
|
} |
|
|
|
void WriteU32LE(std::byte *destination, uint32_t value) |
|
{ |
|
auto *bytes = reinterpret_cast<uint8_t *>(destination); |
|
bytes[0] = static_cast<uint8_t>(value & 0xFF); |
|
bytes[1] = static_cast<uint8_t>((value >> 8) & 0xFF); |
|
bytes[2] = static_cast<uint8_t>((value >> 16) & 0xFF); |
|
bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF); |
|
} |
|
|
|
bool IsSaveContainer(const std::byte *data, size_t size) |
|
{ |
|
if (size < SaveHeaderSize) |
|
return false; |
|
|
|
const auto *bytes = reinterpret_cast<const char *>(data); |
|
return bytes[0] == SaveMagic[0] |
|
&& bytes[1] == SaveMagic[1] |
|
&& bytes[2] == SaveMagic[2] |
|
&& bytes[3] == SaveMagic[3] |
|
&& static_cast<uint8_t>(bytes[4]) == SaveFormatVersion; |
|
} |
|
|
|
bool DecodeZlib( |
|
const std::byte *input, |
|
size_t inputSize, |
|
std::byte *output, |
|
size_t outputCapacity, |
|
size_t &decodedSize) |
|
{ |
|
if (input == nullptr || output == nullptr || outputCapacity == 0) |
|
return false; |
|
if (inputSize > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
|
return false; |
|
if (outputCapacity > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
|
return false; |
|
|
|
uLongf size = static_cast<uLongf>(outputCapacity); |
|
const int rc = uncompress( |
|
reinterpret_cast<Bytef *>(output), |
|
&size, |
|
reinterpret_cast<const Bytef *>(input), |
|
static_cast<uLong>(inputSize)); |
|
if (rc != Z_OK) |
|
return false; |
|
if (size != outputCapacity) |
|
return false; |
|
|
|
decodedSize = static_cast<size_t>(size); |
|
return true; |
|
} |
|
|
|
std::unique_ptr<std::byte[]> BuildSaveContainer(const std::byte *data, size_t size, size_t &outSize) |
|
{ |
|
outSize = 0; |
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize) |
|
return nullptr; |
|
if (size > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
|
return nullptr; |
|
|
|
SaveCodec codec = SaveCodec::Raw; |
|
size_t payloadSize = size; |
|
std::unique_ptr<std::byte[]> compressed; |
|
uLongf compressedSize = compressBound(static_cast<uLong>(size)); |
|
|
|
if (compressedSize > 0) { |
|
compressed = std::make_unique<std::byte[]>(compressedSize); |
|
const int rc = compress2( |
|
reinterpret_cast<Bytef *>(compressed.get()), |
|
&compressedSize, |
|
reinterpret_cast<const Bytef *>(data), |
|
static_cast<uLong>(size), |
|
Z_BEST_SPEED); |
|
if (rc == Z_OK && compressedSize < size) { |
|
codec = SaveCodec::Zlib; |
|
payloadSize = static_cast<size_t>(compressedSize); |
|
} |
|
} |
|
|
|
if (payloadSize > std::numeric_limits<uint32_t>::max()) |
|
return nullptr; |
|
if (size > std::numeric_limits<uint32_t>::max()) |
|
return nullptr; |
|
|
|
const size_t containerSize = SaveHeaderSize + payloadSize; |
|
auto container = std::make_unique<std::byte[]>(containerSize); |
|
|
|
auto *bytes = reinterpret_cast<char *>(container.get()); |
|
bytes[0] = SaveMagic[0]; |
|
bytes[1] = SaveMagic[1]; |
|
bytes[2] = SaveMagic[2]; |
|
bytes[3] = SaveMagic[3]; |
|
bytes[4] = static_cast<char>(SaveFormatVersion); |
|
bytes[5] = static_cast<char>(codec); |
|
bytes[6] = 0; |
|
bytes[7] = 0; |
|
WriteU32LE(container.get() + 8, static_cast<uint32_t>(payloadSize)); |
|
WriteU32LE(container.get() + 12, static_cast<uint32_t>(size)); |
|
|
|
if (codec == SaveCodec::Zlib) { |
|
std::memcpy(container.get() + SaveHeaderSize, compressed.get(), payloadSize); |
|
LogVerbose("[DC Save] zlib compressed {} -> {} bytes ({:.1f}%)", |
|
size, payloadSize, 100.0f * payloadSize / size); |
|
} else { |
|
std::memcpy(container.get() + SaveHeaderSize, data, payloadSize); |
|
LogVerbose("[DC Save] Stored {} bytes as raw payload", size); |
|
} |
|
|
|
outSize = containerSize; |
|
return container; |
|
} |
|
|
|
std::unique_ptr<std::byte[]> DecodeSaveContainer( |
|
const std::byte *data, |
|
size_t size, |
|
size_t &outSize, |
|
const char *sourceTag) |
|
{ |
|
outSize = 0; |
|
if (!IsSaveContainer(data, size)) { |
|
LogError("[DC Save] Invalid save container format in {}", sourceTag); |
|
return nullptr; |
|
} |
|
|
|
const uint8_t codec = static_cast<uint8_t>(reinterpret_cast<const char *>(data)[5]); |
|
const uint32_t payloadSize = ReadU32LE(data + 8); |
|
const uint32_t originalSize = ReadU32LE(data + 12); |
|
|
|
if (originalSize == 0 || originalSize > MaxSaveDataSize) { |
|
LogError("[DC Save] Invalid container original size {} in {}", originalSize, sourceTag); |
|
return nullptr; |
|
} |
|
if (payloadSize > size - SaveHeaderSize) { |
|
LogError("[DC Save] Invalid container payload size {} in {}", payloadSize, sourceTag); |
|
return nullptr; |
|
} |
|
|
|
const std::byte *payload = data + SaveHeaderSize; |
|
auto decoded = std::make_unique<std::byte[]>(originalSize); |
|
|
|
if (codec == static_cast<uint8_t>(SaveCodec::Raw)) { |
|
if (payloadSize < originalSize) { |
|
LogError("[DC Save] Raw payload too small in {}", sourceTag); |
|
return nullptr; |
|
} |
|
std::memcpy(decoded.get(), payload, originalSize); |
|
outSize = originalSize; |
|
return decoded; |
|
} |
|
|
|
if (codec == static_cast<uint8_t>(SaveCodec::Zlib)) { |
|
size_t decodedSize = 0; |
|
if (!DecodeZlib(payload, payloadSize, decoded.get(), originalSize, decodedSize)) { |
|
LogError("[DC Save] zlib decode failed for {}", sourceTag); |
|
return nullptr; |
|
} |
|
outSize = decodedSize; |
|
return decoded; |
|
} |
|
|
|
LogError("[DC Save] Unknown container codec {} in {}", codec, sourceTag); |
|
return nullptr; |
|
} |
|
|
|
bool ReadFileBytes(const char *path, std::unique_ptr<std::byte[]> &buffer, size_t &size) |
|
{ |
|
buffer.reset(); |
|
size = 0; |
|
|
|
FILE *file = std::fopen(path, "rb"); |
|
if (file == nullptr) |
|
return false; |
|
|
|
if (std::fseek(file, 0, SEEK_END) != 0) { |
|
std::fclose(file); |
|
return false; |
|
} |
|
const long fileLen = std::ftell(file); |
|
if (fileLen <= 0) { |
|
std::fclose(file); |
|
return false; |
|
} |
|
if (std::fseek(file, 0, SEEK_SET) != 0) { |
|
std::fclose(file); |
|
return false; |
|
} |
|
|
|
size = static_cast<size_t>(fileLen); |
|
buffer = std::make_unique<std::byte[]>(size); |
|
if (std::fread(buffer.get(), size, 1, file) != 1) { |
|
std::fclose(file); |
|
buffer.reset(); |
|
size = 0; |
|
return false; |
|
} |
|
|
|
std::fclose(file); |
|
return true; |
|
} |
|
|
|
} // namespace |
|
|
|
bool WriteCompressedFile(const char *path, const std::byte *data, size_t size) |
|
{ |
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize || size > std::numeric_limits<uint32_t>::max()) { |
|
LogError("[DC Save] Refusing to write invalid save payload ({} bytes) to {}", size, path); |
|
return false; |
|
} |
|
|
|
size_t containerSize = 0; |
|
auto container = BuildSaveContainer(data, size, containerSize); |
|
if (!container || containerSize == 0) { |
|
LogError("[DC Save] Failed to create save container for {}", path); |
|
return false; |
|
} |
|
|
|
FILE *file = std::fopen(path, "wb"); |
|
if (file == nullptr) { |
|
LogError("[DC Save] Failed to open {} for writing", path); |
|
return false; |
|
} |
|
|
|
if (std::fwrite(container.get(), containerSize, 1, file) != 1) { |
|
LogError("[DC Save] Failed to write container data to {}", path); |
|
std::fclose(file); |
|
return false; |
|
} |
|
|
|
std::fclose(file); |
|
return true; |
|
} |
|
|
|
std::unique_ptr<std::byte[]> ReadCompressedFile(const char *path, size_t &outSize) |
|
{ |
|
std::unique_ptr<std::byte[]> fileBytes; |
|
size_t fileSize = 0; |
|
if (!ReadFileBytes(path, fileBytes, fileSize)) { |
|
outSize = 0; |
|
return nullptr; |
|
} |
|
return DecodeSaveContainer(fileBytes.get(), fileSize, outSize, path); |
|
} |
|
|
|
// Blank 32x32 icon (4bpp = 512 bytes) for VMU file display. |
|
static uint8_t g_blankIcon[512] = { 0 }; |
|
|
|
bool WriteToVmu(const char *vmuPath, const char *filename, |
|
const std::byte *data, size_t size) |
|
{ |
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize) |
|
return false; |
|
|
|
size_t payloadSize = 0; |
|
auto payload = BuildSaveContainer(data, size, payloadSize); |
|
if (!payload || payloadSize == 0) { |
|
LogError("[DC VMU] Failed to create save container for {}", filename); |
|
return false; |
|
} |
|
|
|
vmu_pkg_t pkg; |
|
std::memset(&pkg, 0, sizeof(pkg)); |
|
std::strncpy(pkg.desc_short, "DevilutionX", sizeof(pkg.desc_short) - 1); |
|
std::strncpy(pkg.desc_long, "DevilutionX Save Data", sizeof(pkg.desc_long) - 1); |
|
std::strncpy(pkg.app_id, "DevilutionX", sizeof(pkg.app_id) - 1); |
|
pkg.icon_cnt = 1; |
|
pkg.icon_anim_speed = 0; |
|
pkg.icon_data = g_blankIcon; |
|
pkg.eyecatch_type = VMUPKG_EC_NONE; |
|
|
|
const std::string fullPath = std::string(vmuPath) + filename; |
|
fs_unlink(fullPath.c_str()); |
|
|
|
file_t fd = fs_open(fullPath.c_str(), O_WRONLY); |
|
if (fd == FILEHND_INVALID) { |
|
LogError("[DC VMU] Cannot open {} for writing", fullPath); |
|
return false; |
|
} |
|
|
|
fs_vmu_set_header(fd, &pkg); |
|
const ssize_t written = fs_write(fd, payload.get(), payloadSize); |
|
const int closeRet = fs_close(fd); |
|
|
|
if (written < 0 || written != static_cast<ssize_t>(payloadSize)) { |
|
LogError("[DC VMU] Short write to {}: {} of {}", fullPath, written, payloadSize); |
|
return false; |
|
} |
|
if (closeRet < 0) { |
|
LogError("[DC VMU] Close failed for {} (VMU full?)", fullPath); |
|
return false; |
|
} |
|
|
|
LogVerbose("[DC VMU] Saved {} ({} -> {} bytes on VMU)", fullPath, size, payloadSize); |
|
return true; |
|
} |
|
|
|
std::unique_ptr<std::byte[]> ReadFromVmu(const char *vmuPath, const char *filename, |
|
size_t &outSize) |
|
{ |
|
outSize = 0; |
|
const std::string fullPath = std::string(vmuPath) + filename; |
|
|
|
file_t fd = fs_open(fullPath.c_str(), O_RDONLY); |
|
if (fd == FILEHND_INVALID) |
|
return nullptr; |
|
|
|
size_t total = fs_total(fd); |
|
if (total == static_cast<size_t>(-1) || total < sizeof(uint32_t)) { |
|
fs_close(fd); |
|
return nullptr; |
|
} |
|
|
|
auto rawBuf = std::make_unique<std::byte[]>(total); |
|
const ssize_t bytesRead = fs_read(fd, rawBuf.get(), total); |
|
fs_close(fd); |
|
|
|
if (bytesRead < static_cast<ssize_t>(sizeof(uint32_t))) |
|
return nullptr; |
|
|
|
return DecodeSaveContainer(rawBuf.get(), static_cast<size_t>(bytesRead), outSize, fullPath.c_str()); |
|
} |
|
|
|
bool VmuFileExists(const char *vmuPath, const char *filename) |
|
{ |
|
const std::string fullPath = std::string(vmuPath) + filename; |
|
file_t fd = fs_open(fullPath.c_str(), O_RDONLY); |
|
if (fd == FILEHND_INVALID) |
|
return false; |
|
fs_close(fd); |
|
return true; |
|
} |
|
|
|
} // namespace dc |
|
} // namespace devilution |
|
|
|
#endif // __DREAMCAST__
|
|
|