From 5a63b8ad2fcf8c0e19adf8d6edd8d8c3ef30dd30 Mon Sep 17 00:00:00 2001 From: Michael Villani <45660891+michvllni@users.noreply.github.com> Date: Fri, 29 May 2026 13:06:02 +0000 Subject: [PATCH] fix: detect save changes when emulator freezes mtime - Add computeMd5Hex() helper using mbedtls_md5 (already linked via mbedcrypto) that hashes a local file into a lowercase hex string - In performSync(), fall back to an MD5 content comparison when localMtime is unchanged but a driveMd5 from the last sync is known - Fixes GBA (and other) emulators that write save data without ever updating the FAT32 modification timestamp, causing saves to be silently skipped on every subsequent sync --- source/main.cpp | 45 +++++++++++++++++++++++++++++++++++++++ source/modules/manifest.h | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/source/main.cpp b/source/main.cpp index 22a68c2..86264ca 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -11,6 +11,7 @@ #include <3ds.h> #include +#include #include "libs/inih/INIReader/INIReader.h" #include "modules/dropbox.h" @@ -265,6 +266,37 @@ static bool waitForMainMenuKey() // --------------------------------------------------------------------------- static bool g_cancelRequested = false; +// --------------------------------------------------------------------------- +// computeMd5Hex — return the MD5 of a local file as a lowercase hex string, +// or "" on error. Used to detect content changes when mtime is stale +// (FAT32 has 2-second granularity and some emulators never update mtime). +// --------------------------------------------------------------------------- +static std::string computeMd5Hex(const std::string &path) +{ + FILE *fp = fopen(path.c_str(), "rb"); + if (!fp) + return ""; + + mbedtls_md5_context ctx; + mbedtls_md5_init(&ctx); + mbedtls_md5_starts(&ctx); + + unsigned char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) + mbedtls_md5_update(&ctx, buf, n); + fclose(fp); + + unsigned char digest[16]; + mbedtls_md5_finish(&ctx, digest); + mbedtls_md5_free(&ctx); + + char hex[33]; + for (int i = 0; i < 16; i++) + snprintf(hex + i * 2, 3, "%02x", digest[i]); + return std::string(hex, 32); +} + // --------------------------------------------------------------------------- // performSync — bidirectional sync for one SyncEntry // Returns false if a fatal Drive error occurred or cancellation was requested. @@ -328,6 +360,19 @@ static bool performSync(GoogleDrive &drive, Manifest &manifest, const SyncEntry bool localChanged = inManifest && (localMtime != mEntry.localMtime); bool driveChanged = inManifest && driveExists && (dfi->md5 != mEntry.driveMd5); + // FAT32 mtime has 2-second granularity and some emulators never update + // the timestamp at all. If mtime is unchanged but we have a Drive MD5 + // from the last sync, compare file content as a reliable fallback. + if (!localChanged && inManifest && localExists && !mEntry.driveMd5.empty()) + { + std::string localMd5 = computeMd5Hex(localPath); + if (!localMd5.empty() && localMd5 != mEntry.driveMd5) + { + printf(" (content changed, mtime frozen — uploading)\n"); + localChanged = true; + } + } + printf(" %s: local=%s drive=%s manifest=%s\n", relPath.c_str(), localExists ? "yes" : "no", diff --git a/source/modules/manifest.h b/source/modules/manifest.h index e309373..2cd5580 100644 --- a/source/modules/manifest.h +++ b/source/modules/manifest.h @@ -8,7 +8,7 @@ // Record of one file's last-known sync state. struct ManifestEntry { - time_t localMtime; // st_mtime at the time of the last successful sync + time_t localMtime; // st_mtime at the time of the last successful sync std::string driveMd5; // md5Checksum returned by Drive after the last sync std::string driveId; // Drive file ID (for update/download without a name search) };