#include "dumper.h"
#include "unscrambler.h"
#include "xbox_ref/xbox_ref_log.h"
+#include "redump_dat.h"
+
+#ifdef WIN32
+#include <sys/stat.h>
+#include <io.h>
+#include <windows.h>
+#else
+#include <sys/stat.h>
+#include <unistd.h>
+#endif
#define printf xbox_ref_printf
#define fprintf xbox_ref_log_fprintf
bool active;
bool dump_attempted;
bool seed_ok;
+ bool seed_applicable;
bool have_seed_duration;
double seed_duration;
bool dump_ok;
} friidump_validation_summary;
static friidump_validation_summary g_validation_summary;
+static redump_verify_result g_redump_result;
+static bool g_redump_attempted = false;
+static bool g_operation_duration_override_valid = false;
+static double g_operation_duration_override = 0.0;
static void friidump_summary_copy(char *dst, size_t dst_size, const char *src) {
if (!dst || dst_size == 0)
static void friidump_summary_reset(void) {
memset(&g_validation_summary, 0, sizeof(g_validation_summary));
g_validation_summary.fail_sector = 0xFFFFFFFFU;
+ g_validation_summary.seed_applicable = true;
+ g_operation_duration_override_valid = false;
+ g_operation_duration_override = 0.0;
}
static void friidump_format_hms(double seconds, char *buf, size_t buf_size) {
static void friidump_print_validation_summary(double duration, bool have_duration) {
double mib_total, mib_per_hour;
+ const char *seed_status;
if (!g_validation_summary.active)
return;
+ seed_status = g_validation_summary.seed_applicable
+ ? (g_validation_summary.seed_ok ? "OK" : "FAILED/NOT REACHED")
+ : "N/A (Xbox reference auth path)";
mib_total = (double) g_validation_summary.sectors * 2048.0 / 1024.0 / 1024.0;
mib_per_hour = (have_duration && duration > 0.0) ? (mib_total / duration * 3600.0) : 0.0;
fprintf(stderr,
" Cache base........: 0x%08x\n"
" Memory windows....: %u\n"
" Disc type.........: %s\n"
- " Game ID...........: %s\n"
+ " Game/Media ID.....: %s\n"
" Title.............: %s\n"
" Output............: %s\n"
" Seed read.........: %s\n"
g_validation_summary.game_id[0] ? g_validation_summary.game_id : "(unknown)",
g_validation_summary.title[0] ? g_validation_summary.title : "(unknown)",
g_validation_summary.output[0] ? g_validation_summary.output : "(none)",
- g_validation_summary.seed_ok ? "OK" : "FAILED/NOT REACHED",
+ seed_status,
g_validation_summary.dump_attempted ? (g_validation_summary.dump_ok ? "OK" : "FAILED") : "NOT ATTEMPTED",
g_validation_summary.stop_attempted ? (g_validation_summary.stop_ok ? "OK" : "FAILED") : "NOT ATTEMPTED");
if (g_validation_summary.have_seed_duration) {
fprintf(stderr, " Duration..........: %.2f seconds\n", duration);
if (have_duration && mib_per_hour > 0.0)
fprintf(stderr, " Observed average..: %.2f MiB/h over %.2f MiB ISO payload\n", mib_per_hour, mib_total);
- if (g_validation_summary.dump_ok)
- fprintf(stderr, " Hash compare......: compare printed hashes against known-good target for this disc\n");
+ if (g_validation_summary.dump_ok) {
+ if (g_redump_attempted) {
+ fprintf(stderr, " Redump verify.....: %s\n", redump_verify_overall_string(&g_redump_result));
+ if (g_redump_result.status == REDUMP_VERIFY_MATCH)
+ fprintf(stderr, " Redump title......: %s\n", g_redump_result.game_name);
+ } else {
+ fprintf(stderr, " Redump verify.....: NOT RUN\n");
+ }
+ }
}
#define PACKAGE_NAME "FriiDump"
/* Define to the version of this package. */
-#define PACKAGE_VERSION "0.5.3.5"
+#define PACKAGE_VERSION "0.5.3.10"
#ifdef WIN32
char *hlds_e7_scan_log;
char *hlds_e7_scan_dump_prefix;
char *hlds_profile_report;
+ char *redump_dat_dir;
+ char *redump_report;
+ bool no_redump_verify;
} options;
}
+
+static const char *friidump_redump_dat_basename(disc_type type_id) {
+ switch (type_id) {
+ case DISC_TYPE_GAMECUBE:
+ return "Nintendo - GameCube.dat";
+ case DISC_TYPE_WII:
+ case DISC_TYPE_WII_DL:
+ return "Nintendo - Wii.dat";
+ case DISC_TYPE_XBOX:
+ return "Microsoft - Xbox.dat";
+ default:
+ return NULL;
+ }
+}
+
+static uint64_t friidump_file_size(const char *path) {
+ if (!path || !path[0])
+ return 0;
+#ifdef WIN32
+ {
+ struct _stat64 st;
+ if (_stat64(path, &st) == 0)
+ return (uint64_t)st.st_size;
+ }
+#else
+ {
+ struct stat st;
+ if (stat(path, &st) == 0)
+ return (uint64_t)st.st_size;
+ }
+#endif
+ return 0;
+}
+
+static bool friidump_sync_report_file(FILE *f) {
+ if (!f || fflush(f) != 0)
+ return false;
+#ifdef WIN32
+ return _commit(_fileno(f)) == 0;
+#else
+ return fsync(fileno(f)) == 0;
+#endif
+}
+
+static bool friidump_atomic_replace(const char *temporary_path, const char *final_path) {
+ if (!temporary_path || !final_path)
+ return false;
+#ifdef WIN32
+ return MoveFileExA(temporary_path, final_path,
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
+#else
+ return rename(temporary_path, final_path) == 0;
+#endif
+}
+
+static void friidump_json_write_string_value(FILE *f, const char *prefix,
+ const char *value, const char *suffix) {
+ fputs(prefix, f);
+ friidump_json_string(f, value ? value : "");
+ fputs(suffix, f);
+}
+
+static void friidump_write_redump_report(const redump_verify_result *result,
+ const char *output_path,
+ uint64_t output_size,
+ const char *crc32,
+ const char *md5,
+ const char *sha1,
+ const char *sha256,
+ const char *hash_source,
+ const char *representation_note) {
+ FILE *f;
+ char *temporary_path;
+ char line[256];
+ size_t temporary_path_size;
+ bool write_ok;
+
+ if (!options.redump_report || !options.redump_report[0] || !result)
+ return;
+
+ temporary_path_size = strlen(options.redump_report) + 5;
+ temporary_path = (char *)malloc(temporary_path_size);
+ if (!temporary_path) {
+ fprintf(stderr, "Redump report.......: memory allocation failed\n");
+ return;
+ }
+ snprintf(temporary_path, temporary_path_size, "%s.tmp", options.redump_report);
+ remove(temporary_path);
+
+ f = fopen(temporary_path, "wb");
+ if (!f) {
+ fprintf(stderr, "Redump report.......: could not write temporary file %s\n", temporary_path);
+ free(temporary_path);
+ return;
+ }
+
+ /* Use fputs/fputc rather than the logged fprintf wrapper. Report contents
+ * belong in the JSON artifact, not as a malformed partial mirror in the
+ * human-readable run log. */
+ fputs("{\n", f);
+ fputs(" \"schema\": 2,\n", f);
+ friidump_json_write_string_value(f, " \"producer\": ", "friidump-" PACKAGE_VERSION, ",\n");
+ friidump_json_write_string_value(f, " \"status\": ", redump_verify_status_string(result->status), ",\n");
+ friidump_json_write_string_value(f, " \"overall\": ", redump_verify_overall_string(result), ",\n");
+ friidump_json_write_string_value(f, " \"confidence\": ", redump_verify_confidence_string(result), ",\n");
+ friidump_json_write_string_value(f, " \"output\": ", output_path ? output_path : "", ",\n");
+ snprintf(line, sizeof(line), " \"output_size\": %llu,\n", (unsigned long long)output_size);
+ fputs(line, f);
+ friidump_json_write_string_value(f, " \"crc32\": ", crc32 ? crc32 : "", ",\n");
+ friidump_json_write_string_value(f, " \"md5\": ", md5 ? md5 : "", ",\n");
+ friidump_json_write_string_value(f, " \"sha1\": ", sha1 ? sha1 : "", ",\n");
+ friidump_json_write_string_value(f, " \"sha256\": ", sha256 ? sha256 : "", ",\n");
+ friidump_json_write_string_value(f, " \"hash_source\": ", hash_source ? hash_source : "", ",\n");
+ friidump_json_write_string_value(f, " \"representation_note\": ", representation_note ? representation_note : "", ",\n");
+ friidump_json_write_string_value(f, " \"dat_path\": ", result->dat_path, ",\n");
+ snprintf(line, sizeof(line), " \"entries_scanned\": %lu,\n", result->entries_scanned);
+ fputs(line, f);
+ snprintf(line, sizeof(line), " \"exact_matches\": %lu,\n", result->exact_matches);
+ fputs(line, f);
+ friidump_json_write_string_value(f, " \"match_kind\": ", redump_verify_match_kind_string(result), ",\n");
+ friidump_json_write_string_value(f, " \"game_name\": ", result->game_name, ",\n");
+ friidump_json_write_string_value(f, " \"rom_name\": ", result->rom_name, ",\n");
+
+ fputs(" \"field_match_counts\": {\n", f);
+ snprintf(line, sizeof(line), " \"size\": %lu,\n", result->size_matches); fputs(line, f);
+ snprintf(line, sizeof(line), " \"crc32\": %lu,\n", result->crc32_matches); fputs(line, f);
+ snprintf(line, sizeof(line), " \"md5\": %lu,\n", result->md5_matches); fputs(line, f);
+ snprintf(line, sizeof(line), " \"sha1\": %lu\n", result->sha1_matches); fputs(line, f);
+ fputs(" },\n", f);
+
+ fputs(" \"expected\": {\n", f);
+ friidump_json_write_string_value(f, " \"size\": ", result->expected_size, ",\n");
+ friidump_json_write_string_value(f, " \"crc32\": ", result->expected_crc32, ",\n");
+ friidump_json_write_string_value(f, " \"md5\": ", result->expected_md5, ",\n");
+ friidump_json_write_string_value(f, " \"sha1\": ", result->expected_sha1, "\n");
+ fputs(" },\n", f);
+
+ fputs(" \"evidence\": {\n", f);
+ friidump_json_write_string_value(f, " \"size\": ", redump_field_status_string(result->size_status), ",\n");
+ friidump_json_write_string_value(f, " \"crc32\": ", redump_field_status_string(result->crc32_status), ",\n");
+ friidump_json_write_string_value(f, " \"md5\": ", redump_field_status_string(result->md5_status), ",\n");
+ friidump_json_write_string_value(f, " \"sha1\": ", redump_field_status_string(result->sha1_status), "\n");
+ fputs(" },\n", f);
+ friidump_json_write_string_value(f, " \"detail\": ", result->detail, "\n");
+ fputs("}\n", f);
+
+ write_ok = !ferror(f) && friidump_sync_report_file(f);
+ if (fclose(f) != 0)
+ write_ok = false;
+
+ if (!write_ok) {
+ remove(temporary_path);
+ fprintf(stderr, "Redump report.......: write/flush failed; final report was not replaced\n");
+ free(temporary_path);
+ return;
+ }
+
+ if (!friidump_atomic_replace(temporary_path, options.redump_report)) {
+ fprintf(stderr, "Redump report.......: could not replace %s; complete temporary report retained at %s\n",
+ options.redump_report, temporary_path);
+ free(temporary_path);
+ return;
+ }
+
+ fprintf(stderr, "Redump report.......: %s (atomic write)\n", options.redump_report);
+ free(temporary_path);
+}
+
+static void friidump_print_redump_field(const char *label, redump_field_status status) {
+ fprintf(stderr, "%-20s %s\n", label, redump_field_status_string(status));
+}
+
+static void friidump_verify_redump_values(disc_type type_id,
+ const char *output_path,
+ uint64_t output_size,
+ const char *crc32,
+ const char *md5,
+ const char *sha1,
+ const char *sha256,
+ const char *hash_source) {
+ const char *basename;
+ const char *dat_dir;
+ const char *representation_note;
+ char dat_path[1024];
+
+ redump_verify_result_init(&g_redump_result);
+ g_redump_attempted = false;
+ representation_note = (type_id == DISC_TYPE_XBOX)
+ ? "XGD1 acquisition success and exact Redump hash identity are separate claims; documented synthetic reconstruction ranges may prevent an exact match."
+ : "";
+
+ if (options.no_redump_verify) {
+ fprintf(stderr, "Redump verification: DISABLED (--no-redump-verify)\n");
+ return;
+ }
+ if (options.no_hashing) {
+ fprintf(stderr, "Redump verification: SKIPPED (hashing disabled)\n");
+ return;
+ }
+ if (!output_path || !output_path[0]) {
+ fprintf(stderr, "Redump verification: SKIPPED (final output path unavailable)\n");
+ return;
+ }
+ if (!crc32 || !crc32[0] || !md5 || !md5[0] || !sha1 || !sha1[0]) {
+ fprintf(stderr, "Redump verification: SKIPPED (finalized CRC32/MD5/SHA-1 evidence incomplete)\n");
+ return;
+ }
+
+ basename = friidump_redump_dat_basename(type_id);
+ if (!basename) {
+ fprintf(stderr, "Redump verification: SKIPPED (no DAT mapping for this disc type)\n");
+ return;
+ }
+
+ dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0]) ? options.redump_dat_dir : "redump_dat";
+ snprintf(dat_path, sizeof(dat_path), "%s/%s", dat_dir, basename);
+ if (output_size == 0)
+ output_size = friidump_file_size(output_path);
+
+ g_redump_attempted = true;
+ redump_verify_dat_file(dat_path, output_size, crc32, md5, sha1, &g_redump_result);
+
+ fprintf(stderr, "\nRedump verification\n");
+ fprintf(stderr, "------------------------------------------------------------\n");
+ fprintf(stderr, "%-20s %s\n", "DAT", g_redump_result.dat_path);
+ fprintf(stderr, "%-20s %s\n", "Hash source", hash_source ? hash_source : "Finalized output hashes");
+ fprintf(stderr, "%-20s %lu\n", "Entries scanned", g_redump_result.entries_scanned);
+
+ if (g_redump_result.status == REDUMP_VERIFY_MATCH) {
+ fprintf(stderr, "%-20s %s\n", "Matched entry", g_redump_result.game_name);
+ fprintf(stderr, "%-20s %s\n", "ROM", g_redump_result.rom_name);
+ friidump_print_redump_field("Image size", g_redump_result.size_status);
+ friidump_print_redump_field("CRC32", g_redump_result.crc32_status);
+ friidump_print_redump_field("MD5", g_redump_result.md5_status);
+ friidump_print_redump_field("SHA-1", g_redump_result.sha1_status);
+ if (g_redump_result.exact_matches > 1)
+ fprintf(stderr, "%-20s %lu exact entries\n", "Duplicate matches", g_redump_result.exact_matches);
+ } else if (g_redump_result.candidate_available) {
+ fprintf(stderr, "%-20s %s\n", "Closest candidate", g_redump_result.game_name);
+ fprintf(stderr, "%-20s %s\n", "ROM", g_redump_result.rom_name);
+ friidump_print_redump_field("Image size", g_redump_result.size_status);
+ friidump_print_redump_field("CRC32", g_redump_result.crc32_status);
+ friidump_print_redump_field("MD5", g_redump_result.md5_status);
+ friidump_print_redump_field("SHA-1", g_redump_result.sha1_status);
+ } else {
+ fprintf(stderr, "%-20s %s\n", "Closest candidate", "None (no hash-correlated entry)");
+ fprintf(stderr, "%-20s %lu entries\n", "Size matches", g_redump_result.size_matches);
+ fprintf(stderr, "%-20s %lu entries\n", "CRC32 matches", g_redump_result.crc32_matches);
+ fprintf(stderr, "%-20s %lu entries\n", "MD5 matches", g_redump_result.md5_matches);
+ fprintf(stderr, "%-20s %lu entries\n", "SHA-1 matches", g_redump_result.sha1_matches);
+ }
+
+ fprintf(stderr, "%-20s %s\n", "Overall", redump_verify_overall_string(&g_redump_result));
+ fprintf(stderr, "%-20s %s\n", "Confidence", redump_verify_confidence_string(&g_redump_result));
+ if (g_redump_result.status != REDUMP_VERIFY_MATCH)
+ fprintf(stderr, "%-20s %s\n", "Detail", g_redump_result.detail);
+ if (type_id == DISC_TYPE_XBOX)
+ fprintf(stderr, "%-20s %s\n", "Representation note", representation_note);
+
+ friidump_write_redump_report(&g_redump_result, output_path, output_size,
+ crc32, md5, sha1, sha256, hash_source,
+ representation_note);
+}
+
+static void friidump_verify_redump_iso(dumper *dmp, disc_type type_id) {
+ const char *output_path;
+
+ if (!dmp)
+ return;
+
+ output_path = options.iso_out;
+ friidump_verify_redump_values(type_id,
+ output_path,
+ friidump_file_size(output_path),
+ dumper_get_iso_crc32(dmp),
+ dumper_get_iso_md5(dmp),
+ dumper_get_iso_sha1(dmp),
+ dumper_get_iso_sha2(dmp),
+ "FriiDump finalized ISO multihash");
+}
+
+static void friidump_verify_redump_xbox_reference(dumper *dmp) {
+ xbox_ref_dump_result result;
+
+ xbox_ref_dump_result_init(&result);
+ if (!dumper_get_xbox_reference_result(dmp, &result)) {
+ fprintf(stderr, "Redump verification: SKIPPED (Xbox reference result handoff unavailable)\n");
+ return;
+ }
+ if (result.mode != '1') {
+ fprintf(stderr, "Redump verification: SKIPPED (Xbox XISO is not a full Redump disc image)\n");
+ return;
+ }
+ if (!result.dump_success) {
+ fprintf(stderr, "Redump verification: SKIPPED (Xbox reference dump did not complete)\n");
+ return;
+ }
+ if (!result.hashes_complete) {
+ fprintf(stderr, "Redump verification: SKIPPED (Xbox finalized full-file hashes incomplete)\n");
+ return;
+ }
+
+ if (g_validation_summary.active) {
+ if (result.output_path[0])
+ friidump_summary_copy(g_validation_summary.output, sizeof(g_validation_summary.output), result.output_path);
+ if (result.title[0])
+ friidump_summary_copy(g_validation_summary.title, sizeof(g_validation_summary.title), result.title);
+ if (result.media_id[0])
+ friidump_summary_copy(g_validation_summary.game_id, sizeof(g_validation_summary.game_id), result.media_id);
+ if (result.output_sectors)
+ g_validation_summary.sectors = result.output_sectors;
+ g_validation_summary.seed_applicable = false;
+ g_validation_summary.have_seed_duration = false;
+ }
+ if (result.elapsed_seconds > 0.0) {
+ g_operation_duration_override_valid = true;
+ g_operation_duration_override = result.elapsed_seconds;
+ }
+
+ friidump_verify_redump_values(DISC_TYPE_XBOX,
+ result.output_path,
+ result.output_size,
+ result.crc32,
+ result.md5,
+ result.sha1,
+ result.sha256,
+ "GDR-8050L reference finalized full-file hashes");
+}
+
void help (void) {
/* 80 cols guide:
* |-------------------------------------------------------------------------------|
" --scan-log <file> JSON output path for --hlds-e7-scan\n"
" --scan-dump-prefix <prefix> Optional raw 0xE7 window dump prefix for --hlds-e7-scan\n"
" --hlds-profile-report <file> Write selected HLDS profile/evidence JSON\n"
+ " --redump-dat-dir <dir> Directory containing canonical Redump DAT files\n"
+ " (default: redump_dat)\n"
+ " --redump-report <file> Write atomic Redump evidence JSON\n"
+ " --no-redump-verify Disable automatic post-dump DAT verification\n"
" -A, --allmethods Try all known command/method combinations until\n"
" one works. Reopens the drive for each command so\n"
" command-specific vendor handlers are rebound.\n"
{"scan-log", 1, 0, 1001},
{"scan-dump-prefix", 1, 0, 1002},
{"hlds-profile-report", 1, 0, 1005},
+ {"redump-dat-dir", 1, 0, 1006},
+ {"redump-report", 1, 0, 1007},
+ {"no-redump-verify", 0, 0, 1008},
#ifdef DEBUG
/* We don't want newbies to generate and put into circulation bad dumps, so this options are disabled for releases */
{"donottunscramble", 0, 0, 'n'},
options.hlds_e7_scan_log = NULL;
options.hlds_e7_scan_dump_prefix = NULL;
options.hlds_profile_report = NULL;
+ options.redump_dat_dir = NULL;
+ options.redump_report = NULL;
+ options.no_redump_verify = false;
do {
#ifdef DEBUG
case 1005:
my_strdup (options.hlds_profile_report, optarg);
break;
+ case 1006:
+ my_strdup (options.redump_dat_dir, optarg);
+ break;
+ case 1007:
+ my_strdup (options.redump_report, optarg);
+ break;
+ case 1008:
+ options.no_redump_verify = true;
+ break;
case -1:
break;
default:
dumper_get_iso_sha1 (dmp), dumper_get_iso_sha2 (dmp)/*, dumper_get_iso_ed2k (dmp)*/
);
+ if (type_id == DISC_TYPE_XBOX && disc_is_xbox_challenge_drive (d) && options.iso_requested)
+ friidump_verify_redump_xbox_reference(dmp);
+ else if (options.iso_out)
+ friidump_verify_redump_iso(dmp, type_id);
+
out = true;
if (g_validation_summary.active)
g_validation_summary.dump_ok = true;
duration += ((double) us / (double) USECS_PER_SEC);
if (duration < 0)
duration = 0;
+ if (g_operation_duration_override_valid)
+ duration = g_operation_duration_override;
fprintf (stderr, "Operation took %.2f seconds\n", duration);
friidump_print_validation_summary(duration, true);