From 82ae2bce9da102e57637f37300721f323e44f56f Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 17 Aug 2026 21:28:56 -0500 Subject: [PATCH] Add compressed combined save format for PSP and Dolphin Memory sticks/cards don't suit N+1 separate save files the way Linux's filesystem does, so add an opt-in SAVE_DEVICE_DATA_RAW mode: settings and all save slots get serialized to JSON, concatenated, zlib-compressed, and framed with a magic/version/checksum/generation header, then read/written as a single blob through one combined platform hook instead of four. - PSP and Dolphin's SD/NAND backends write via a temp file + atomic rename, so a crash mid-write can never leave a half-written save behind. - GameCube memory cards have no rename or resize primitive, so they ping-pong between two fixed files instead, picking whichever is valid and has the higher generation counter on load. - Linux is untouched (still four separate JSON files); saveslot.h/ savesettings.h drop their now-unnecessary pack(1) now that nothing persists them as raw bytes. Co-Authored-By: Claude Sonnet 5 --- src/dusk/save/savedevice.c | 451 ++++++++++++++++++- src/dusk/save/savedevice.h | 49 +- src/dusk/save/savesettings.h | 2 - src/dusk/save/saveslot.h | 2 - src/duskdolphin/save/savedevicedolphincard.c | 154 +++++++ src/duskdolphin/save/savedevicedolphincard.h | 49 ++ src/duskdolphin/save/savedevicedolphinnand.c | 105 +++++ src/duskdolphin/save/savedevicedolphinnand.h | 36 ++ src/duskdolphin/save/savedevicedolphinsd.c | 92 ++++ src/duskdolphin/save/savedevicedolphinsd.h | 36 ++ src/duskdolphin/save/savedeviceplatform.h | 20 + src/duskpsp/save/savedeviceplatform.h | 15 + src/duskpsp/save/savedevicepsp.c | 98 ++++ src/duskpsp/save/savedevicepsp.h | 33 ++ 14 files changed, 1125 insertions(+), 17 deletions(-) diff --git a/src/dusk/save/savedevice.c b/src/dusk/save/savedevice.c index cf17b5c4..131ec1e0 100644 --- a/src/dusk/save/savedevice.c +++ b/src/dusk/save/savedevice.c @@ -9,6 +9,16 @@ #include "assert/assert.h" #include "util/memory.h" +#if defined(SAVE_DEVICE_DATA_RAW) + #include "yyjson.h" + #include "save/savejson.h" + #include "save/saveslot.h" + #include "save/savesettings.h" + #include "util/crypt.h" + #include "util/endian.h" + #include +#endif + errorret_t saveDeviceInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -79,6 +89,415 @@ void saveDeviceCheckAvailability( saveDeviceCheckAvailabilityPlatform(device); } +#if defined(SAVE_DEVICE_DATA_RAW) + +// Bumped to 2 for the addition of `generation` below. +#define SAVE_DEVICE_RAW_VERSION ((uint32_t)2) +// Sanity cap against a corrupted/hostile header driving a bogus allocation. +#define SAVE_DEVICE_RAW_MAX_SIZE ((uint32_t)(256 * 1024)) + +static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'}; + +#pragma pack(push, 1) +typedef struct { + char_t magic[4]; + uint32_t version; + uint32_t uncompressedSize;// size of the logical blob, pre-compression + uint32_t compressedSize;// size of the payload following this header + uint32_t checksum;// cryptCRC32() over the compressed payload + // Monotonically increasing with every store. Unused by single-file + // platforms (rename already guarantees the current file is the latest), + // but load-bearing for platforms with no rename primitive and multiple + // physical copies - e.g. GameCube memory cards ping-ponging between two + // fixed files, where this is how saveDeviceRawIsValid() picks the newest + // valid copy. + uint32_t generation; +} savedevicerawheader_t; +#pragma pack(pop) + +typedef struct { + const uint8_t *ptr; + uint32_t len; +} savedevicerawspan_t; + +typedef struct { + savedevicerawspan_t settings; + savedevicerawspan_t slots[SAVE_SLOT_COUNT]; +} savedevicerawspans_t; + +// A single settings/slot item's JSON bytes, either borrowed from an existing +// decompressed blob (owned == NULL) or freshly serialized just now via +// yyjson_mut_write (owned != NULL, must be freed with plain free()). +typedef struct { + const uint8_t *ptr; + uint32_t len; + char_t *owned; +} savedevicerawitem_t; + +// Parses the length-prefixed span table at the front of a decompressed +// logical blob. Spans point directly into `logical`, nothing is copied. +static void saveDeviceRawParseSpans( + const uint8_t *logical, + savedevicerawspans_t *spans +) { + size_t offset = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT); + + spans->settings.len = endianLittleToHost32(*(const uint32_t *)(logical + 0)); + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + spans->slots[i].len = endianLittleToHost32( + *(const uint32_t *)(logical + sizeof(uint32_t) * (1 + i)) + ); + } + + spans->settings.ptr = logical + offset; + offset += spans->settings.len; + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + spans->slots[i].ptr = logical + offset; + offset += spans->slots[i].len; + } +} + +// Cheaply checks whether `raw` is a well-formed, checksum-valid raw save +// blob, without decompressing it - just header + checksum validation. Used +// both by saveDeviceRawInflate below and, publicly (see savedevice.h), by +// platforms that keep more than one physical copy and need to pick the +// newest valid one (e.g. GameCube memory cards - see `generation` above). +bool_t saveDeviceRawIsValid( + const uint8_t *raw, + const size_t rawSize, + uint32_t *outGeneration +) { + if(raw == NULL || rawSize < sizeof(savedevicerawheader_t)) return false; + + const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw; + if(memoryCompare(header->magic, SAVE_DEVICE_RAW_MAGIC, 4) != 0) { + return false; + } + if(endianLittleToHost32(header->version) != SAVE_DEVICE_RAW_VERSION) { + return false; + } + + uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize); + uint32_t compressedSize = endianLittleToHost32(header->compressedSize); + if( + uncompressedSize > SAVE_DEVICE_RAW_MAX_SIZE || + compressedSize > SAVE_DEVICE_RAW_MAX_SIZE || + // Trailing bytes beyond the declared payload are tolerated as padding - + // e.g. a GameCube memory card file rounded up to a whole sector. + rawSize < sizeof(savedevicerawheader_t) + compressedSize + ) { + return false; + } + + uint32_t checksum = cryptCRC32(raw + sizeof(savedevicerawheader_t), compressedSize); + if(checksum != endianLittleToHost32(header->checksum)) return false; + + if(outGeneration != NULL) { + *outGeneration = endianLittleToHost32(header->generation); + } + return true; +} + +// Validates the header and inflates the compressed payload that follows it. +// On success, *outLogical is a memoryAllocate'd buffer the caller must free. +static errorret_t saveDeviceRawInflate( + const uint8_t *raw, + const size_t rawSize, + uint8_t **outLogical, + size_t *outLogicalSize, + uint32_t *outGeneration +) { + if(!saveDeviceRawIsValid(raw, rawSize, outGeneration)) { + errorThrow("Save data failed validation (magic/version/size/checksum)"); + } + + const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw; + uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize); + uint32_t compressedSize = endianLittleToHost32(header->compressedSize); + const uint8_t *compressed = raw + sizeof(savedevicerawheader_t); + + uint8_t *logical = memoryAllocate(uncompressedSize); + uLongf destLen = (uLongf)uncompressedSize; + int result = uncompress( + logical, &destLen, compressed, (uLong)compressedSize + ); + if(result != Z_OK || destLen != uncompressedSize) { + memoryFree(logical); + errorThrow("Failed to decompress save data (zlib error %d)", result); + } + + *outLogical = logical; + *outLogicalSize = uncompressedSize; + errorOk(); +} + +// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the +// result must be freed with plain free(), not memoryFree(). +static errorret_t saveDeviceRawSerializeSettings( + savesettings_t *settings, + char_t **outJson, + size_t *outLen +) { + writeInit(); + + errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object); + if(errorIsNotOk(writeResult)) { + yyjson_mut_doc_free(doc); + errorChain(writeResult); + } + + *outJson = yyjson_mut_write(doc, 0, outLen); + yyjson_mut_doc_free(doc); + if(*outJson == NULL) errorThrow("Failed to write settings JSON"); + + errorOk(); +} + +// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the +// result must be freed with plain free(), not memoryFree(). +static errorret_t saveDeviceRawSerializeSlot( + saveslot_t *slot, + char_t **outJson, + size_t *outLen +) { + writeInit(); + + errorret_t writeResult = saveSlotWriteJSON(slot, doc, object); + if(errorIsNotOk(writeResult)) { + yyjson_mut_doc_free(doc); + errorChain(writeResult); + } + + *outJson = yyjson_mut_write(doc, 0, outLen); + yyjson_mut_doc_free(doc); + if(*outJson == NULL) errorThrow("Failed to write slot JSON"); + + errorOk(); +} + +// Frees whatever's been collected so far - safe to call at any point since +// unset items are zero-initialized (owned == NULL is a no-op skip). +static void saveDeviceRawCleanupStore( + uint8_t *oldRaw, + uint8_t *oldLogical, + savedevicerawitem_t *settingsItem, + savedevicerawitem_t *slotItems +) { + if(oldRaw != NULL) memoryFree(oldRaw); + if(oldLogical != NULL) memoryFree(oldLogical); + if(settingsItem->owned != NULL) free(settingsItem->owned); + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + if(slotItems[i].owned != NULL) free(slotItems[i].owned); + } +} + +// Reads the existing combined blob (if any), replaces exactly one item - +// `settings`, or the slot at `slotIndex` when `slot` is given, exactly one +// of the two must be non-NULL - and rewrites the whole blob. Every other +// item's JSON is carried through byte-for-byte from what was already +// stored (or freshly defaulted if nothing was stored yet), so a save never +// reconstructs data it wasn't given. +static errorret_t saveDeviceRawStoreItem( + savedevice_t *device, + savesettings_t *settings, + saveslot_t *slot, + const uint8_t slotIndex +) { + uint8_t *oldRaw = NULL; + size_t oldRawSize = 0; + errorChain(saveDeviceDataReadPlatform(device, &oldRaw, &oldRawSize)); + + uint8_t *oldLogical = NULL; + size_t oldLogicalSize = 0; + uint32_t oldGeneration = 0; + savedevicerawspans_t oldSpans; + bool_t haveOld = oldRaw != NULL; + if(haveOld) { + errorret_t inflateResult = saveDeviceRawInflate( + oldRaw, oldRawSize, &oldLogical, &oldLogicalSize, &oldGeneration + ); + if(errorIsNotOk(inflateResult)) { + memoryFree(oldRaw); + errorChain(inflateResult); + } + saveDeviceRawParseSpans(oldLogical, &oldSpans); + } + + savedevicerawitem_t settingsItem = {0}; + savedevicerawitem_t slotItems[SAVE_SLOT_COUNT] = {0}; + + if(settings != NULL) { + char_t *json = NULL; + size_t len = 0; + errorret_t result = saveDeviceRawSerializeSettings(settings, &json, &len); + if(errorIsNotOk(result)) { + saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); + errorChain(result); + } + settingsItem = (savedevicerawitem_t){ + .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json + }; + } else if(haveOld) { + settingsItem.ptr = oldSpans.settings.ptr; + settingsItem.len = oldSpans.settings.len; + } else { + savesettings_t defaultSettings; + saveSettingsInit(&defaultSettings); + char_t *json = NULL; + size_t len = 0; + errorret_t result = saveDeviceRawSerializeSettings( + &defaultSettings, &json, &len + ); + if(errorIsNotOk(result)) { + saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); + errorChain(result); + } + settingsItem = (savedevicerawitem_t){ + .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json + }; + } + + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + if(slot != NULL && i == slotIndex) { + char_t *json = NULL; + size_t len = 0; + errorret_t result = saveDeviceRawSerializeSlot(slot, &json, &len); + if(errorIsNotOk(result)) { + saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); + errorChain(result); + } + slotItems[i] = (savedevicerawitem_t){ + .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json + }; + } else if(haveOld) { + slotItems[i].ptr = oldSpans.slots[i].ptr; + slotItems[i].len = oldSpans.slots[i].len; + } else { + saveslot_t defaultSlot; + saveSlotInit(&defaultSlot); + char_t *json = NULL; + size_t len = 0; + errorret_t result = saveDeviceRawSerializeSlot(&defaultSlot, &json, &len); + if(errorIsNotOk(result)) { + saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); + errorChain(result); + } + slotItems[i] = (savedevicerawitem_t){ + .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json + }; + } + } + + // Assemble the new logical blob: length table, then each item's bytes. + size_t tableSize = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT); + size_t logicalSize = tableSize + settingsItem.len; + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) logicalSize += slotItems[i].len; + + uint8_t *newLogical = memoryAllocate(logicalSize); + *(uint32_t *)(newLogical + 0) = endianLittleToHost32(settingsItem.len); + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + *(uint32_t *)(newLogical + sizeof(uint32_t) * (1 + i)) = + endianLittleToHost32(slotItems[i].len); + } + + size_t offset = tableSize; + memoryCopy(newLogical + offset, settingsItem.ptr, settingsItem.len); + offset += settingsItem.len; + for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { + memoryCopy(newLogical + offset, slotItems[i].ptr, slotItems[i].len); + offset += slotItems[i].len; + } + + saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); + + // Compress and frame with the header. + uLongf compressedCap = compressBound((uLong)logicalSize); + uint8_t *compressedBuf = memoryAllocate(compressedCap); + uLongf compressedLen = compressedCap; + int compressResult = compress2( + compressedBuf, &compressedLen, newLogical, (uLong)logicalSize, + Z_DEFAULT_COMPRESSION + ); + memoryFree(newLogical); + if(compressResult != Z_OK) { + memoryFree(compressedBuf); + errorThrow("Failed to compress save data (zlib error %d)", compressResult); + } + + savedevicerawheader_t header; + memoryCopy(header.magic, SAVE_DEVICE_RAW_MAGIC, 4); + header.version = endianLittleToHost32(SAVE_DEVICE_RAW_VERSION); + header.uncompressedSize = endianLittleToHost32((uint32_t)logicalSize); + header.compressedSize = endianLittleToHost32((uint32_t)compressedLen); + header.checksum = endianLittleToHost32(cryptCRC32(compressedBuf, compressedLen)); + header.generation = endianLittleToHost32(haveOld ? oldGeneration + 1 : 1); + + size_t finalSize = sizeof(header) + compressedLen; + uint8_t *finalBuf = memoryAllocate(finalSize); + memoryCopy(finalBuf, &header, sizeof(header)); + memoryCopy(finalBuf + sizeof(header), compressedBuf, compressedLen); + memoryFree(compressedBuf); + + errorret_t writeResult = saveDeviceDataWritePlatform( + device, finalBuf, finalSize + ); + memoryFree(finalBuf); + errorChain(writeResult); + + errorOk(); +} + +// Reads the combined blob and populates exactly one item - `settings`, or +// the slot at `slotIndex` when `slot` is given, exactly one of the two must +// be non-NULL. Leaves the destination untouched if nothing's been saved yet. +static errorret_t saveDeviceRawFetchItem( + savedevice_t *device, + savesettings_t *settings, + saveslot_t *slot, + const uint8_t slotIndex +) { + uint8_t *raw = NULL; + size_t rawSize = 0; + errorChain(saveDeviceDataReadPlatform(device, &raw, &rawSize)); + if(raw == NULL) errorOk(); + + uint8_t *logical = NULL; + size_t logicalSize = 0; + errorret_t inflateResult = saveDeviceRawInflate( + raw, rawSize, &logical, &logicalSize, NULL + ); + memoryFree(raw); + errorChain(inflateResult); + + savedevicerawspans_t spans; + saveDeviceRawParseSpans(logical, &spans); + const savedevicerawspan_t *span = settings != NULL ? + &spans.settings : &spans.slots[slotIndex]; + + yyjson_doc *jsonDoc = yyjson_read((const char_t *)span->ptr, span->len, 0); + if(jsonDoc == NULL) { + memoryFree(logical); + errorThrow("Failed to parse save data item JSON"); + } + + yyjson_val *object = yyjson_doc_get_root(jsonDoc); + if(object == NULL) { + yyjson_doc_free(jsonDoc); + memoryFree(logical); + errorThrow("Save data item JSON missing root object"); + } + + errorret_t readResult = settings != NULL ? + saveSettingsReadJSON(settings, object) : saveSlotReadJSON(slot, object); + yyjson_doc_free(jsonDoc); + memoryFree(logical); + errorChain(readResult); + + errorOk(); +} + +#endif// defined(SAVE_DEVICE_DATA_RAW) + errorret_t saveDeviceSlotWrite( savedevice_t *device, saveslot_t *slot, @@ -87,9 +506,11 @@ errorret_t saveDeviceSlotWrite( assertNotNull(device, "device cannot be null"); assertNotNull(slot, "slot cannot be null"); -#ifdef saveDeviceSlotWritePlatform - errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex)); -#endif + #if defined(SAVE_DEVICE_DATA_RAW) + errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex)); + #elif defined(saveDeviceSlotWritePlatform) + errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex)); + #endif errorOk(); } @@ -102,9 +523,11 @@ errorret_t saveDeviceSlotRead( assertNotNull(device, "device cannot be null"); assertNotNull(slot, "slot cannot be null"); -#ifdef saveDeviceSlotReadPlatform - errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex)); -#endif + #if defined(SAVE_DEVICE_DATA_RAW) + errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex)); + #elif defined(saveDeviceSlotReadPlatform) + errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex)); + #endif errorOk(); } @@ -116,9 +539,11 @@ errorret_t saveDeviceSettingsWrite( assertNotNull(device, "device cannot be null"); assertNotNull(settings, "settings cannot be null"); -#ifdef saveDeviceSettingsWritePlatform - errorChain(saveDeviceSettingsWritePlatform(device, settings)); -#endif + #if defined(SAVE_DEVICE_DATA_RAW) + errorChain(saveDeviceRawStoreItem(device, settings, NULL, 0)); + #elif defined(saveDeviceSettingsWritePlatform) + errorChain(saveDeviceSettingsWritePlatform(device, settings)); + #endif errorOk(); } @@ -130,9 +555,11 @@ errorret_t saveDeviceSettingsRead( assertNotNull(device, "device cannot be null"); assertNotNull(settings, "settings cannot be null"); -#ifdef saveDeviceSettingsReadPlatform - errorChain(saveDeviceSettingsReadPlatform(device, settings)); -#endif + #if defined(SAVE_DEVICE_DATA_RAW) + errorChain(saveDeviceRawFetchItem(device, settings, NULL, 0)); + #elif defined(saveDeviceSettingsReadPlatform) + errorChain(saveDeviceSettingsReadPlatform(device, settings)); + #endif errorOk(); } diff --git a/src/dusk/save/savedevice.h b/src/dusk/save/savedevice.h index ae6e9de6..f153a597 100644 --- a/src/dusk/save/savedevice.h +++ b/src/dusk/save/savedevice.h @@ -14,6 +14,34 @@ #error "SAVE_DEVICE_COUNT must be defined" #endif +// Platform opt-in: define this in a platform's savedeviceplatform.h (instead +// of the four saveDevice{Slot,Settings}{Write,Read}Platform macros) for +// devices without free-form filesystem access - e.g. a memory card/stick +// that suits one combined save blob better than N+1 separate files. When +// defined, the platform must instead provide these two: +// +// errorret_t saveDeviceDataWritePlatform( +// savedevice_t *device, const uint8_t *buffer, size_t size +// ); +// errorret_t saveDeviceDataReadPlatform( +// savedevice_t *device, uint8_t **outBuffer, size_t *outSize +// ); +// +// savedevice.c compresses settings + all save slots' JSON into one +// magic/version/checksum-framed blob and read/writes it as a single raw +// buffer through these two hooks instead. saveDeviceDataReadPlatform must +// allocate its output with memoryAllocate (ownership passes to the caller, +// who frees it with memoryFree), and set *outBuffer = NULL/*outSize = 0 +// (not an error) when nothing has been saved yet - same convention as the +// "file not found" case in the non-raw four-hook mode. +// #define SAVE_DEVICE_DATA_RAW +// +// saveDeviceDataReadPlatform must always resolve to "the one current blob" +// even if the platform keeps more than one physical copy under the hood - +// e.g. a platform with no rename primitive (GameCube memory cards) that +// ping-pongs between two fixed files instead. saveDeviceRawIsValid() below +// is what such a platform uses to tell which of its copies is newest. + typedef struct savedevice_s savedevice_t; typedef struct savesettings_s savesettings_t; typedef struct saveslot_s saveslot_t; @@ -132,4 +160,23 @@ errorret_t saveDeviceSettingsRead( * @param device The save device to dispose. * @return Error state if any. */ -errorret_t saveDeviceDispose(savedevice_t *device); \ No newline at end of file +errorret_t saveDeviceDispose(savedevice_t *device); + +/** + * Only relevant to SAVE_DEVICE_DATA_RAW platforms that keep more than one + * physical copy of the save blob (see the note above) - cheaply checks + * whether `raw` is a well-formed, checksum-valid raw save blob without + * decompressing it, and if so reports its generation counter so the caller + * can tell which of several copies is newest. + * + * @param raw The raw bytes to validate. + * @param rawSize The number of bytes in raw. + * @param outGeneration Receives the blob's generation counter if valid, may + * be NULL if the caller doesn't need it. + * @return true if raw is a valid, checksum-passing save blob. + */ +bool_t saveDeviceRawIsValid( + const uint8_t *raw, + const size_t rawSize, + uint32_t *outGeneration +); \ No newline at end of file diff --git a/src/dusk/save/savesettings.h b/src/dusk/save/savesettings.h index 1a1b711d..0d83fe30 100644 --- a/src/dusk/save/savesettings.h +++ b/src/dusk/save/savesettings.h @@ -10,11 +10,9 @@ #include "savedevice.h" #include "yyjson.h" -#pragma pack(push, 1) typedef struct savesettings_s { int32_t someSetting; } savesettings_t; -#pragma pack(pop) /** * Inits the save settings with the default state, this is functionally "new diff --git a/src/dusk/save/saveslot.h b/src/dusk/save/saveslot.h index 3c2dc841..52d1ca67 100644 --- a/src/dusk/save/saveslot.h +++ b/src/dusk/save/saveslot.h @@ -13,7 +13,6 @@ #define SAVE_SLOT_NAME_LENGTH 8 #define SAVE_SLOT_COUNT 3 -#pragma pack(push, 1) typedef struct { char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator dusktimeepoch_t time; @@ -26,7 +25,6 @@ typedef struct saveslot_s { saveslotcache_t cachedData; } saveslot_t; -#pragma pack(pop) /** * Inits the save slot with the default state, this is functionally "new game" diff --git a/src/duskdolphin/save/savedevicedolphincard.c b/src/duskdolphin/save/savedevicedolphincard.c index c7325260..af6d35a2 100644 --- a/src/duskdolphin/save/savedevicedolphincard.c +++ b/src/duskdolphin/save/savedevicedolphincard.c @@ -9,6 +9,7 @@ #include "save/savedevicedolphincard.h" #include "save/save.h" #include "assert/assert.h" +#include "util/memory.h" errorret_t saveDeviceDolphinCardInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -128,6 +129,159 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) { return usedBlocks < (uint32_t)blockCount; } +// One probe result for one of the two ping-pong slots. +typedef struct { + bool_t valid; + uint32_t generation; + uint8_t *buffer;// only set if keepBuffer was requested, else NULL + uint32_t len; +} savedevicedolphincardslot_t; + +// Opens the given card file (if it exists) and validates it as a raw save +// blob via saveDeviceRawIsValid(). CARD_Read requires a 32-byte aligned +// buffer, so the read (and any buffer kept for the caller) uses memoryAlign +// rather than memoryAllocate. If keepBuffer is false, or the file doesn't +// exist/validate, no buffer is left allocated. +static savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( + const int32_t channel, + const char_t *filename, + const bool_t keepBuffer +) { + savedevicedolphincardslot_t result = {0}; + + card_file file; + if(CARD_Open(channel, filename, &file) < 0) return result; + + uint32_t len = (uint32_t)file.len; + uint8_t *buffer = memoryAlign(32, len); + int32_t readResult = CARD_Read(&file, buffer, len, 0); + CARD_Close(&file); + + if(readResult < 0 || !saveDeviceRawIsValid(buffer, len, &result.generation)) { + memoryFree(buffer); + return result; + } + + result.valid = true; + result.len = len; + if(keepBuffer) { + result.buffer = buffer; + } else { + memoryFree(buffer); + } + return result; +} + +errorret_t saveDeviceDolphinCardDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(buffer, "buffer cannot be null"); + + int32_t channel = device->platform.channel; + + savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot( + channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, false + ); + savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot( + channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, false + ); + + // Target whichever slot is NOT the currently valid+newest one, so a crash + // partway through this write can never touch the copy a load would use. + const char_t *targetFilename; + if(slot0.valid && slot1.valid) { + targetFilename = slot0.generation <= slot1.generation ? + SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 : SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1; + } else if(slot0.valid) { + targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1; + } else { + targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0; + } + + uint32_t sectorSize = 0; + if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) { + errorThrow("Failed to query memory card sector size"); + } + + // CARD_Create's size must be a sector-size multiple, and CARD_Read always + // reports that full (possibly padded) size back via file.len - harmless, + // since saveDeviceRawIsValid() tolerates trailing padding beyond the + // header's declared payload length. + uint32_t paddedSize = + (uint32_t)(((size + sectorSize - 1) / sectorSize) * sectorSize); + uint8_t *paddedBuffer = memoryAlign(32, paddedSize); + memorySet(paddedBuffer, 0, paddedSize); + memoryCopy(paddedBuffer, buffer, size); + + // The target file may already exist from an earlier generation at a + // different size - memory card files can't be resized, so it has to be + // deleted and recreated. This only ever touches the stale/inactive slot, + // never the one saveDeviceDolphinCardDataRead would currently pick. + CARD_Delete(channel, targetFilename);// Not an error if it doesn't exist. + + card_file file; + int32_t createResult = CARD_Create( + channel, targetFilename, paddedSize, &file + ); + if(createResult < 0) { + memoryFree(paddedBuffer); + errorThrow("Failed to create save data file: %s", targetFilename); + } + + int32_t writeResult = CARD_Write(&file, paddedBuffer, paddedSize, 0); + CARD_Close(&file); + memoryFree(paddedBuffer); + if(writeResult < 0) errorThrow("Failed to write save data: %s", targetFilename); + + errorOk(); +} + +errorret_t saveDeviceDolphinCardDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(outBuffer, "outBuffer cannot be null"); + assertNotNull(outSize, "outSize cannot be null"); + + int32_t channel = device->platform.channel; + + savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot( + channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, true + ); + savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot( + channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, true + ); + + savedevicedolphincardslot_t *chosen = NULL; + if(slot0.valid && slot1.valid) { + chosen = slot0.generation >= slot1.generation ? &slot0 : &slot1; + } else if(slot0.valid) { + chosen = &slot0; + } else if(slot1.valid) { + chosen = &slot1; + } + + if(chosen == NULL) { + // Neither slot has ever been saved to (or both failed validation) - + // not an error, leave the destination as-is. + *outBuffer = NULL; + *outSize = 0; + errorOk(); + } + + if(chosen != &slot0 && slot0.buffer != NULL) memoryFree(slot0.buffer); + if(chosen != &slot1 && slot1.buffer != NULL) memoryFree(slot1.buffer); + + *outBuffer = chosen->buffer; + *outSize = chosen->len; + errorOk(); +} + errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) { assertNotNull(device, "device cannot be null"); if(device->platform.mounted) { diff --git a/src/duskdolphin/save/savedevicedolphincard.h b/src/duskdolphin/save/savedevicedolphincard.h index 7eabc6d5..a5a720ba 100644 --- a/src/duskdolphin/save/savedevicedolphincard.h +++ b/src/duskdolphin/save/savedevicedolphincard.h @@ -17,6 +17,20 @@ #define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK" #endif +// Memory cards have no rename primitive and files can't be resized once +// created, so a single "current" file can't be replaced atomically the way +// the other backends do. Instead saveDeviceDolphinCardDataWrite() ping-pongs +// between these two fixed files - every store targets whichever one is NOT +// the currently valid+newest copy (by saveDeviceRawIsValid()'s generation +// counter), so a crash mid-write can only ever corrupt the copy nobody +// would load from; the other one stays intact as the fallback. +#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 + #define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 "DUSKSAVE0" +#endif +#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 + #define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 "DUSKSAVE1" +#endif + typedef struct { // Set from this device's index into SAVE.devices - device 0 is // CARD_SLOTA, device 1 is CARD_SLOTB. @@ -82,3 +96,38 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel); * @return Error state if any. */ errorret_t saveDeviceDolphinCardDispose(savedevice_t *device); + +/** + * Writes the combined save data blob out to whichever of the two fixed + * card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not + * currently the valid+newest copy - ping-pong in place of the atomic + * rename the other backends use, since the CARD API has neither a rename + * nor a way to resize an existing file. + * + * @param device The save device to write to. + * @param buffer The raw bytes to write. + * @param size The number of bytes to write. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinCardDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +); + +/** + * Reads whichever of the two fixed card files is valid and has the higher + * generation counter. If neither file exists or validates, this is not an + * error - *outBuffer is set to NULL and *outSize to 0. + * + * @param device The save device to read from. + * @param outBuffer Receives a memoryAlign'd (32-byte) buffer the caller + * must free with memoryFree, or NULL if nothing has been saved yet. + * @param outSize Receives the number of bytes in *outBuffer. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinCardDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +); diff --git a/src/duskdolphin/save/savedevicedolphinnand.c b/src/duskdolphin/save/savedevicedolphinnand.c index c668d4ae..65f75802 100644 --- a/src/duskdolphin/save/savedevicedolphinnand.c +++ b/src/duskdolphin/save/savedevicedolphinnand.c @@ -8,6 +8,10 @@ #include "save/savedevice.h" #include "save/savedevicedolphinnand.h" #include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +#define SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX 64 errorret_t saveDeviceDolphinNandInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -62,6 +66,107 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } +static errorret_t saveDeviceDolphinNandGetDataPath( + char_t *dest, + const size_t destSize +) { + assertNotNull(dest, "dest cannot be null"); + + stringFormat( + dest, destSize, "%s/%s", + SAVE_DEVICE_DOLPHIN_NAND_DIR, SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME + ); + + errorOk(); +} + +errorret_t saveDeviceDolphinNandDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(buffer, "buffer cannot be null"); + + char_t finalPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; + errorChain(saveDeviceDolphinNandGetDataPath(finalPath, sizeof(finalPath))); + + char_t tempPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; + stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); + + // Best-effort create - unlike POSIX's O_CREAT, ISFS_Open can't create a + // file that doesn't exist yet, so ISFS_CreateFile has to run first. Best + // effort since a leftover temp file from a previous crash is fine to + // clobber. + ISFS_Delete(tempPath); + int32_t createResult = ISFS_CreateFile( + tempPath, 0, ISFS_OPEN_RW, ISFS_OPEN_RW, ISFS_OPEN_RW + ); + if(createResult != ISFS_OK) { + errorThrow("Failed to create save data file: %s", tempPath); + } + + int32_t fd = ISFS_Open(tempPath, ISFS_OPEN_WRITE); + if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath); + + int32_t written = ISFS_Write(fd, buffer, (u32)size); + ISFS_Close(fd); + if(written < 0 || (size_t)written != size) { + errorThrow("Failed to write save data: %s", tempPath); + } + + // Atomic replace: the temp file is fully written before it ever takes the + // final name, so a crash/power-loss here can never leave a half-written + // save behind - at worst the previous save survives untouched. + ISFS_Delete(finalPath);// Not an error if it doesn't exist yet. + if(ISFS_Rename(tempPath, finalPath) != ISFS_OK) { + errorThrow("Failed to finalize save data: %s", finalPath); + } + + errorOk(); +} + +errorret_t saveDeviceDolphinNandDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(outBuffer, "outBuffer cannot be null"); + assertNotNull(outSize, "outSize cannot be null"); + + char_t path[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; + errorChain(saveDeviceDolphinNandGetDataPath(path, sizeof(path))); + + int32_t fd = ISFS_Open(path, ISFS_OPEN_READ); + if(fd < 0) { + // No save yet - not an error. + *outBuffer = NULL; + *outSize = 0; + errorOk(); + } + + fstats stats; + if(ISFS_GetFileStats(fd, &stats) != ISFS_OK) { + ISFS_Close(fd); + errorThrow("Failed to stat save data: %s", path); + } + + size_t size = (size_t)stats.file_length; + uint8_t *buffer = memoryAllocate(size); + + int32_t readBytes = ISFS_Read(fd, buffer, (u32)size); + ISFS_Close(fd); + if(readBytes < 0 || (size_t)readBytes != size) { + memoryFree(buffer); + errorThrow("Failed to read save data: %s", path); + } + + *outBuffer = buffer; + *outSize = size; + errorOk(); +} + errorret_t saveDeviceDolphinNandDispose(savedevice_t *device) { assertNotNull(device, "device cannot be null"); if(device->platform.initialized) { diff --git a/src/duskdolphin/save/savedevicedolphinnand.h b/src/duskdolphin/save/savedevicedolphinnand.h index 79386fb7..74204628 100644 --- a/src/duskdolphin/save/savedevicedolphinnand.h +++ b/src/duskdolphin/save/savedevicedolphinnand.h @@ -19,6 +19,9 @@ #ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR #define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data" #endif +#ifndef SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME + #define SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME "save.dat" +#endif typedef struct { bool_t initialized; @@ -58,3 +61,36 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device); * @return Error state if any. */ errorret_t saveDeviceDolphinNandDispose(savedevice_t *device); + +/** + * Writes the combined save data blob out to the NAND, replacing it + * atomically (write to a temp file, then swap it in via ISFS_Rename) so a + * crash mid-write can never leave a half-written file behind. + * + * @param device The save device to write to. + * @param buffer The raw bytes to write. + * @param size The number of bytes to write. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinNandDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +); + +/** + * Reads the combined save data blob in from the NAND. If no save data + * exists yet, this is not an error - *outBuffer is set to NULL and + * *outSize to 0. + * + * @param device The save device to read from. + * @param outBuffer Receives a memoryAllocate'd buffer the caller must free + * with memoryFree, or NULL if nothing has been saved yet. + * @param outSize Receives the number of bytes in *outBuffer. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinNandDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +); diff --git a/src/duskdolphin/save/savedevicedolphinsd.c b/src/duskdolphin/save/savedevicedolphinsd.c index 98fa2c20..4096e0dc 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.c +++ b/src/duskdolphin/save/savedevicedolphinsd.c @@ -8,9 +8,14 @@ #include "save/savedevice.h" #include "save/savedevicedolphinsd.h" #include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" #include #include #include +#include + +#define SAVE_DEVICE_DOLPHIN_SD_PATH_MAX 128 errorret_t saveDeviceDolphinSDInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -62,6 +67,93 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } +static errorret_t saveDeviceDolphinSDGetDataPath( + char_t *dest, + const size_t destSize +) { + assertNotNull(dest, "dest cannot be null"); + + stringFormat( + dest, destSize, "%s/%s", + SAVE_DEVICE_DOLPHIN_SD_DIRECTORY, SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME + ); + + errorOk(); +} + +errorret_t saveDeviceDolphinSDDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(buffer, "buffer cannot be null"); + + char_t finalPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; + errorChain(saveDeviceDolphinSDGetDataPath(finalPath, sizeof(finalPath))); + + char_t tempPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; + stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); + + FILE *file = fopen(tempPath, "wb"); + if(file == NULL) errorThrow("Failed to open save data for writing: %s", tempPath); + + size_t written = fwrite(buffer, 1, size, file); + fclose(file); + if(written != size) errorThrow("Failed to write save data: %s", tempPath); + + // Atomic replace: the temp file is fully written before it ever takes the + // final name, so a crash/power-loss here can never leave a half-written + // save behind - at worst the previous save survives untouched. + remove(finalPath);// Not an error if it doesn't exist yet. + if(rename(tempPath, finalPath) != 0) { + errorThrow("Failed to finalize save data: %s", finalPath); + } + + errorOk(); +} + +errorret_t saveDeviceDolphinSDDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(outBuffer, "outBuffer cannot be null"); + assertNotNull(outSize, "outSize cannot be null"); + + char_t path[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; + errorChain(saveDeviceDolphinSDGetDataPath(path, sizeof(path))); + + struct stat st; + if(stat(path, &st) != 0) { + // No save yet - not an error. + *outBuffer = NULL; + *outSize = 0; + errorOk(); + } + + size_t size = (size_t)st.st_size; + uint8_t *buffer = memoryAllocate(size); + + FILE *file = fopen(path, "rb"); + if(file == NULL) { + memoryFree(buffer); + errorThrow("Failed to open save data for reading: %s", path); + } + + size_t readBytes = fread(buffer, 1, size, file); + fclose(file); + if(readBytes != size) { + memoryFree(buffer); + errorThrow("Failed to read save data: %s", path); + } + + *outBuffer = buffer; + *outSize = size; + errorOk(); +} + errorret_t saveDeviceDolphinSDDispose(savedevice_t *device) { errorOk(); } diff --git a/src/duskdolphin/save/savedevicedolphinsd.h b/src/duskdolphin/save/savedevicedolphinsd.h index ad9967b2..192de3fd 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.h +++ b/src/duskdolphin/save/savedevicedolphinsd.h @@ -14,6 +14,9 @@ #ifndef SAVE_DEVICE_DOLPHIN_SD_DIRECTORY #define SAVE_DEVICE_DOLPHIN_SD_DIRECTORY "/dusk/save" #endif +#ifndef SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME + #define SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME "save.dat" +#endif typedef struct { void *nothing; @@ -53,3 +56,36 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device); * @return Error state if any. */ errorret_t saveDeviceDolphinSDDispose(savedevice_t *device); + +/** + * Writes the combined save data blob out to the SD card, replacing it + * atomically (write to a temp file, then swap it in) so a crash mid-write + * can never leave a half-written file behind. + * + * @param device The save device to write to. + * @param buffer The raw bytes to write. + * @param size The number of bytes to write. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinSDDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +); + +/** + * Reads the combined save data blob in from the SD card. If no save data + * exists yet, this is not an error - *outBuffer is set to NULL and + * *outSize to 0. + * + * @param device The save device to read from. + * @param outBuffer Receives a memoryAllocate'd buffer the caller must free + * with memoryFree, or NULL if nothing has been saved yet. + * @param outSize Receives the number of bytes in *outBuffer. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinSDDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +); diff --git a/src/duskdolphin/save/savedeviceplatform.h b/src/duskdolphin/save/savedeviceplatform.h index d3a23c95..857f2d9d 100644 --- a/src/duskdolphin/save/savedeviceplatform.h +++ b/src/duskdolphin/save/savedeviceplatform.h @@ -25,6 +25,12 @@ #define saveDeviceCheckAvailabilityPlatform \ saveDeviceDolphinNandCheckAvailability #define saveDevicePlatformDispose saveDeviceDolphinNandDispose + + // ISFS_Rename exists, so NAND uses the same write-temp-then-rename + // pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW in save/savedevice.h. + #define SAVE_DEVICE_DATA_RAW + #define saveDeviceDataWritePlatform saveDeviceDolphinNandDataWrite + #define saveDeviceDataReadPlatform saveDeviceDolphinNandDataRead #elif defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_SD) #include "savedevicedolphinsd.h" #define SAVE_DEVICE_COUNT 1 @@ -33,6 +39,13 @@ #define saveDeviceCheckAvailabilityPlatform \ saveDeviceDolphinSDCheckAvailability #define saveDevicePlatformDispose saveDeviceDolphinSDDispose + + // libfat gives standard POSIX rename(), so SD uses the same + // write-temp-then-rename pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW + // in save/savedevice.h. + #define SAVE_DEVICE_DATA_RAW + #define saveDeviceDataWritePlatform saveDeviceDolphinSDDataWrite + #define saveDeviceDataReadPlatform saveDeviceDolphinSDDataRead #else #include "savedevicedolphincard.h" #define SAVE_DEVICE_COUNT 2 @@ -41,4 +54,11 @@ #define saveDeviceCheckAvailabilityPlatform \ saveDeviceDolphinCardCheckAvailability #define saveDevicePlatformDispose saveDeviceDolphinCardDispose + + // No rename or resize primitive on the CARD API, so this ping-pongs + // between two fixed files instead - see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 + // in savedevicedolphincard.h and SAVE_DEVICE_DATA_RAW in save/savedevice.h. + #define SAVE_DEVICE_DATA_RAW + #define saveDeviceDataWritePlatform saveDeviceDolphinCardDataWrite + #define saveDeviceDataReadPlatform saveDeviceDolphinCardDataRead #endif diff --git a/src/duskpsp/save/savedeviceplatform.h b/src/duskpsp/save/savedeviceplatform.h index 9918d51f..4942b780 100644 --- a/src/duskpsp/save/savedeviceplatform.h +++ b/src/duskpsp/save/savedeviceplatform.h @@ -14,3 +14,18 @@ #define saveDevicePlatformUpdate saveDevicePSPUpdate #define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability #define saveDevicePlatformDispose saveDevicePSPDispose + +// The memory stick has no directory-listing UI convention to preserve like +// GameCube/Wii memory cards do, so settings + all save slots are combined +// into one compressed blob rather than kept as separate files - see +// SAVE_DEVICE_DATA_RAW in save/savedevice.h. +#define SAVE_DEVICE_DATA_RAW +#define saveDeviceDataWritePlatform saveDevicePSPDataWrite +#define saveDeviceDataReadPlatform saveDevicePSPDataRead + +#ifndef SAVE_DEVICE_PSP_DIRECTORY_NAME + #define SAVE_DEVICE_PSP_DIRECTORY_NAME "ms0:/DUSK" +#endif +#ifndef SAVE_DEVICE_PSP_DATA_FILENAME + #define SAVE_DEVICE_PSP_DATA_FILENAME "save.dat" +#endif diff --git a/src/duskpsp/save/savedevicepsp.c b/src/duskpsp/save/savedevicepsp.c index 848508bd..6240381a 100644 --- a/src/duskpsp/save/savedevicepsp.c +++ b/src/duskpsp/save/savedevicepsp.c @@ -8,8 +8,12 @@ #include "save/savedevice.h" #include "save/savedevicepsp.h" #include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" #include +#define SAVE_DEVICE_PSP_PATH_MAX 64 + errorret_t saveDevicePSPInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -47,6 +51,100 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } +static errorret_t saveDevicePSPGetDataPath( + char_t *dest, + const size_t destSize +) { + assertNotNull(dest, "dest cannot be null"); + + stringFormat( + dest, destSize, "%s/%s", + SAVE_DEVICE_PSP_DIRECTORY_NAME, SAVE_DEVICE_PSP_DATA_FILENAME + ); + + errorOk(); +} + +errorret_t saveDevicePSPDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(buffer, "buffer cannot be null"); + + // Best-effort create - a fresh memory stick won't have this directory yet. + sceIoMkdir(SAVE_DEVICE_PSP_DIRECTORY_NAME, 0777); + + char_t finalPath[SAVE_DEVICE_PSP_PATH_MAX]; + errorChain(saveDevicePSPGetDataPath(finalPath, sizeof(finalPath))); + + char_t tempPath[SAVE_DEVICE_PSP_PATH_MAX]; + stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); + + SceUID fd = sceIoOpen( + tempPath, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777 + ); + if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath); + + int written = sceIoWrite(fd, buffer, (SceSize)size); + sceIoClose(fd); + if(written < 0 || (size_t)written != size) { + errorThrow("Failed to write save data: %s", tempPath); + } + + // Atomic replace: the temp file is fully written before it ever takes the + // final name, so a crash/power-loss here can never leave a half-written + // save behind - at worst the previous save survives untouched. + sceIoRemove(finalPath);// Not an error if it doesn't exist yet. + if(sceIoRename(tempPath, finalPath) < 0) { + errorThrow("Failed to finalize save data: %s", finalPath); + } + + errorOk(); +} + +errorret_t saveDevicePSPDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(device, "device cannot be null"); + assertNotNull(outBuffer, "outBuffer cannot be null"); + assertNotNull(outSize, "outSize cannot be null"); + + char_t path[SAVE_DEVICE_PSP_PATH_MAX]; + errorChain(saveDevicePSPGetDataPath(path, sizeof(path))); + + SceIoStat stat; + if(sceIoGetstat(path, &stat) < 0) { + // No save yet - not an error. + *outBuffer = NULL; + *outSize = 0; + errorOk(); + } + + size_t size = (size_t)stat.st_size; + uint8_t *buffer = memoryAllocate(size); + + SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0); + if(fd < 0) { + memoryFree(buffer); + errorThrow("Failed to open save data for reading: %s", path); + } + + int readBytes = sceIoRead(fd, buffer, (SceSize)size); + sceIoClose(fd); + if(readBytes < 0 || (size_t)readBytes != size) { + memoryFree(buffer); + errorThrow("Failed to read save data: %s", path); + } + + *outBuffer = buffer; + *outSize = size; + errorOk(); +} + errorret_t saveDevicePSPDispose(savedevice_t *device) { errorOk(); } diff --git a/src/duskpsp/save/savedevicepsp.h b/src/duskpsp/save/savedevicepsp.h index 76f9f1d7..8da644a5 100644 --- a/src/duskpsp/save/savedevicepsp.h +++ b/src/duskpsp/save/savedevicepsp.h @@ -27,6 +27,39 @@ typedef struct savedevice_s savedevice_t; */ errorret_t saveDevicePSPInit(savedevice_t *device); +/** + * Writes the combined save data blob out to the memory stick, replacing it + * atomically (write to a temp file, then swap it in) so a crash mid-write + * can never leave a half-written file behind. + * + * @param device The save device to write to. + * @param buffer The raw bytes to write. + * @param size The number of bytes to write. + * @return Error state if any. + */ +errorret_t saveDevicePSPDataWrite( + savedevice_t *device, + const uint8_t *buffer, + size_t size +); + +/** + * Reads the combined save data blob in from the memory stick. If no save + * data exists yet, this is not an error - *outBuffer is set to NULL and + * *outSize to 0. + * + * @param device The save device to read from. + * @param outBuffer Receives a memoryAllocate'd buffer the caller must free + * with memoryFree, or NULL if nothing has been saved yet. + * @param outSize Receives the number of bytes in *outBuffer. + * @return Error state if any. + */ +errorret_t saveDevicePSPDataRead( + savedevice_t *device, + uint8_t **outBuffer, + size_t *outSize +); + /** * Updates the save device platform. *