/** * @file dc_save_codec.cpp * @brief Dreamcast save compression with zlib */ #ifdef __DREAMCAST__ #include "dc_save_codec.hpp" #include "utils/log.hpp" #include #include #include #include #include #include #include #include #include #include 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(data); return static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) | (static_cast(bytes[2]) << 16) | (static_cast(bytes[3]) << 24); } void WriteU32LE(std::byte *destination, uint32_t value) { auto *bytes = reinterpret_cast(destination); bytes[0] = static_cast(value & 0xFF); bytes[1] = static_cast((value >> 8) & 0xFF); bytes[2] = static_cast((value >> 16) & 0xFF); bytes[3] = static_cast((value >> 24) & 0xFF); } bool IsSaveContainer(const std::byte *data, size_t size) { if (size < SaveHeaderSize) return false; const auto *bytes = reinterpret_cast(data); return bytes[0] == SaveMagic[0] && bytes[1] == SaveMagic[1] && bytes[2] == SaveMagic[2] && bytes[3] == SaveMagic[3] && static_cast(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(std::numeric_limits::max())) return false; if (outputCapacity > static_cast(std::numeric_limits::max())) return false; uLongf size = static_cast(outputCapacity); const int rc = uncompress( reinterpret_cast(output), &size, reinterpret_cast(input), static_cast(inputSize)); if (rc != Z_OK) return false; if (size != outputCapacity) return false; decodedSize = static_cast(size); return true; } std::unique_ptr 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(std::numeric_limits::max())) return nullptr; SaveCodec codec = SaveCodec::Raw; size_t payloadSize = size; std::unique_ptr compressed; uLongf compressedSize = compressBound(static_cast(size)); if (compressedSize > 0) { compressed = std::make_unique(compressedSize); const int rc = compress2( reinterpret_cast(compressed.get()), &compressedSize, reinterpret_cast(data), static_cast(size), Z_BEST_SPEED); if (rc == Z_OK && compressedSize < size) { codec = SaveCodec::Zlib; payloadSize = static_cast(compressedSize); } } if (payloadSize > std::numeric_limits::max()) return nullptr; if (size > std::numeric_limits::max()) return nullptr; const size_t containerSize = SaveHeaderSize + payloadSize; auto container = std::make_unique(containerSize); auto *bytes = reinterpret_cast(container.get()); bytes[0] = SaveMagic[0]; bytes[1] = SaveMagic[1]; bytes[2] = SaveMagic[2]; bytes[3] = SaveMagic[3]; bytes[4] = static_cast(SaveFormatVersion); bytes[5] = static_cast(codec); bytes[6] = 0; bytes[7] = 0; WriteU32LE(container.get() + 8, static_cast(payloadSize)); WriteU32LE(container.get() + 12, static_cast(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 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(reinterpret_cast(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(originalSize); if (codec == static_cast(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(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 &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(fileLen); buffer = std::make_unique(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::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 ReadCompressedFile(const char *path, size_t &outSize) { std::unique_ptr 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(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 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(-1) || total < sizeof(uint32_t)) { fs_close(fd); return nullptr; } auto rawBuf = std::make_unique(total); const ssize_t bytesRead = fs_read(fd, rawBuf.get(), total); fs_close(fd); if (bytesRead < static_cast(sizeof(uint32_t))) return nullptr; return DecodeSaveContainer(rawBuf.get(), static_cast(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__