1 #include "redump_dat.h"
8 static int redump_path_exists(const char *path) {
10 if (!path || !path[0])
12 f = fopen(path, "rb");
19 static int redump_join_path(
21 const char *child_dir,
28 if (!dir || !dir[0] || !basename || !basename[0] ||
29 !out_path || out_path_size == 0)
32 if (child_dir && child_dir[0])
33 written = snprintf(out_path, out_path_size, "%s/%s/%s",
34 dir, child_dir, basename);
36 written = snprintf(out_path, out_path_size, "%s/%s",
39 if (written < 0 || (size_t)written >= out_path_size) {
47 int redump_resolve_dat_path(
48 const char *explicit_dir,
49 const char *executable_dir,
54 const char *environment_dir;
57 if (!basename || !basename[0] || !out_path || out_path_size == 0)
62 /* An explicit CLI directory is authoritative. Do not silently fall back
63 * when the operator supplied a path, because that would make evidence
64 * provenance ambiguous. */
65 if (explicit_dir && explicit_dir[0]) {
66 if (!redump_join_path(explicit_dir, NULL, basename,
67 out_path, out_path_size))
69 return redump_path_exists(out_path);
72 /* Environment override is useful for packaged and read-only installs.
73 * Like the CLI override, it is authoritative when set. */
74 environment_dir = getenv("FRIIDUMP_REDUMP_DAT_DIR");
75 if (environment_dir && environment_dir[0]) {
76 if (!redump_join_path(environment_dir, NULL, basename,
77 out_path, out_path_size))
79 return redump_path_exists(out_path);
82 /* Portable/source-build layout: DAT directory beside the executable. */
83 if (executable_dir && executable_dir[0] &&
84 redump_join_path(executable_dir, "redump_dat", basename,
85 candidate, sizeof(candidate))) {
86 if (redump_path_exists(candidate)) {
87 snprintf(out_path, out_path_size, "%s", candidate);
90 snprintf(out_path, out_path_size, "%s", candidate);
93 /* Installed Unix layout: <prefix>/bin/friidump with data under
94 * <prefix>/share/friidump/redump_dat. */
95 if (executable_dir && executable_dir[0] &&
96 redump_join_path(executable_dir, "../share/friidump/redump_dat",
97 basename, candidate, sizeof(candidate))) {
98 if (redump_path_exists(candidate)) {
99 snprintf(out_path, out_path_size, "%s", candidate);
104 /* Development convenience for direct source-tree execution. */
105 if (redump_join_path(".", "redump_dat", basename,
106 candidate, sizeof(candidate))) {
107 if (redump_path_exists(candidate)) {
108 snprintf(out_path, out_path_size, "%s", candidate);
112 snprintf(out_path, out_path_size, "%s", candidate);
118 static int redump_ascii_equal(const char *a, const char *b) {
119 unsigned char ca, cb;
123 ca = (unsigned char)tolower((unsigned char)*a++);
124 cb = (unsigned char)tolower((unsigned char)*b++);
128 return *a == '\0' && *b == '\0';
131 static int redump_extract_attr(const char *line, const char *name, char *out, size_t out_size) {
137 if (!line || !name || !out || out_size == 0)
139 snprintf(pattern, sizeof(pattern), "%s=\"", name);
140 start = strstr(line, pattern);
143 start += strlen(pattern);
144 end = strchr(start, '"');
147 len = (size_t)(end - start);
150 memcpy(out, start, len);
155 static void redump_xml_decode(char *text) {
156 struct entity_s { const char *encoded; const char decoded; };
157 static const struct entity_s entities[] = {
158 {"&", '&'}, {""", '"'}, {"'", '\''},
159 {"<", '<'}, {">", '>'}
164 for (i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) {
166 size_t encoded_len = strlen(entities[i].encoded);
167 while ((p = strstr(text, entities[i].encoded)) != NULL) {
168 *p = entities[i].decoded;
169 memmove(p + 1, p + encoded_len, strlen(p + encoded_len) + 1);
174 static void redump_store_candidate(
175 redump_verify_result *result,
176 const char *game_name,
177 const char *rom_name,
178 const char *size_text,
190 result->candidate_available = 1;
191 result->candidate_hash_matches = hash_matches;
192 result->size_status = size_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
193 result->crc32_status = crc32_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
194 result->md5_status = md5_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
195 result->sha1_status = sha1_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
196 snprintf(result->game_name, sizeof(result->game_name), "%s",
197 (game_name && game_name[0]) ? game_name : ((rom_name && rom_name[0]) ? rom_name : ""));
198 snprintf(result->rom_name, sizeof(result->rom_name), "%s", rom_name ? rom_name : "");
199 snprintf(result->expected_size, sizeof(result->expected_size), "%s", size_text ? size_text : "");
200 snprintf(result->expected_crc32, sizeof(result->expected_crc32), "%s", crc32 ? crc32 : "");
201 snprintf(result->expected_md5, sizeof(result->expected_md5), "%s", md5 ? md5 : "");
202 snprintf(result->expected_sha1, sizeof(result->expected_sha1), "%s", sha1 ? sha1 : "");
205 void redump_verify_result_init(redump_verify_result *result) {
208 memset(result, 0, sizeof(*result));
209 result->status = REDUMP_VERIFY_NOT_RUN;
210 result->size_status = REDUMP_FIELD_NOT_AVAILABLE;
211 result->crc32_status = REDUMP_FIELD_NOT_AVAILABLE;
212 result->md5_status = REDUMP_FIELD_NOT_AVAILABLE;
213 result->sha1_status = REDUMP_FIELD_NOT_AVAILABLE;
216 const char *redump_verify_status_string(redump_verify_status status) {
218 case REDUMP_VERIFY_MATCH: return "MATCH";
219 case REDUMP_VERIFY_NO_MATCH: return "NO MATCH";
220 case REDUMP_VERIFY_DAT_NOT_FOUND: return "DAT NOT FOUND";
221 case REDUMP_VERIFY_DAT_READ_ERROR: return "DAT READ ERROR";
222 case REDUMP_VERIFY_INVALID_INPUT: return "INVALID INPUT";
223 default: return "NOT RUN";
227 const char *redump_field_status_string(redump_field_status status) {
229 case REDUMP_FIELD_PASS: return "PASS";
230 case REDUMP_FIELD_FAIL: return "FAIL";
231 default: return "N/A";
235 const char *redump_verify_overall_string(const redump_verify_result *result) {
237 return "NOT VERIFIED";
238 if (result->status == REDUMP_VERIFY_MATCH)
239 return "VERIFIED AGAINST REDUMP";
240 if (result->status == REDUMP_VERIFY_NO_MATCH)
241 return "NO EXACT MATCH";
242 return "NOT VERIFIED";
245 const char *redump_verify_confidence_string(const redump_verify_result *result) {
247 return "Verification unavailable";
248 if (result->status == REDUMP_VERIFY_MATCH)
249 return "Exact archive match";
250 if (result->status == REDUMP_VERIFY_NO_MATCH && result->candidate_available)
251 return "Partial hash correlation; dump not verified";
252 if (result->status == REDUMP_VERIFY_NO_MATCH)
253 return "No archive identity established";
254 if (result->status == REDUMP_VERIFY_INVALID_INPUT)
255 return "Required evidence unavailable";
256 return "Verification unavailable";
259 const char *redump_verify_match_kind_string(const redump_verify_result *result) {
262 if (result->status == REDUMP_VERIFY_MATCH)
264 if (result->candidate_available)
269 int redump_verify_dat_file(
270 const char *dat_path,
275 redump_verify_result *result
279 char current_game[512] = "";
280 int best_hash_matches = 0;
281 int best_size_match = 0;
285 redump_verify_result_init(result);
288 snprintf(result->dat_path, sizeof(result->dat_path), "%s", dat_path);
289 if (!dat_path || !dat_path[0] || image_size == 0 || !crc32 || !md5 || !sha1 ||
290 !crc32[0] || !md5[0] || !sha1[0]) {
291 result->status = REDUMP_VERIFY_INVALID_INPUT;
292 snprintf(result->detail, sizeof(result->detail),
293 "Output size plus complete CRC32, MD5, and SHA-1 values are required.");
297 fp = fopen(dat_path, "rb");
299 result->status = REDUMP_VERIFY_DAT_NOT_FOUND;
300 snprintf(result->detail, sizeof(result->detail), "Could not open DAT file.");
304 while (fgets(line, sizeof(line), fp)) {
305 if (strstr(line, "<game ") != NULL) {
306 if (redump_extract_attr(line, "name", current_game, sizeof(current_game)))
307 redump_xml_decode(current_game);
311 if (strstr(line, "<rom ") != NULL) {
312 char rom_name[512] = "";
313 char size_text[32] = "";
314 char entry_crc[16] = "";
315 char entry_md5[40] = "";
316 char entry_sha1[48] = "";
324 if (!redump_extract_attr(line, "size", size_text, sizeof(size_text)) ||
325 !redump_extract_attr(line, "crc", entry_crc, sizeof(entry_crc)) ||
326 !redump_extract_attr(line, "md5", entry_md5, sizeof(entry_md5)) ||
327 !redump_extract_attr(line, "sha1", entry_sha1, sizeof(entry_sha1)))
330 redump_extract_attr(line, "name", rom_name, sizeof(rom_name));
331 redump_xml_decode(rom_name);
332 entry_size = (uint64_t)strtoull(size_text, NULL, 10);
334 result->entries_scanned++;
335 size_match = (entry_size == image_size);
336 crc32_match = redump_ascii_equal(crc32, entry_crc);
337 md5_match = redump_ascii_equal(md5, entry_md5);
338 sha1_match = redump_ascii_equal(sha1, entry_sha1);
339 hash_matches = crc32_match + md5_match + sha1_match;
341 if (size_match) result->size_matches++;
342 if (crc32_match) result->crc32_matches++;
343 if (md5_match) result->md5_matches++;
344 if (sha1_match) result->sha1_matches++;
346 if (size_match && crc32_match && md5_match && sha1_match) {
347 result->exact_matches++;
348 if (result->status != REDUMP_VERIFY_MATCH) {
349 result->status = REDUMP_VERIFY_MATCH;
350 redump_store_candidate(result, current_game, rom_name, size_text,
351 entry_crc, entry_md5, entry_sha1,
353 snprintf(result->detail, sizeof(result->detail),
354 "Size, CRC32, MD5, and SHA-1 matched.");
359 if (result->status != REDUMP_VERIFY_MATCH && hash_matches > 0 &&
360 (hash_matches > best_hash_matches ||
361 (hash_matches == best_hash_matches && size_match > best_size_match))) {
362 best_hash_matches = hash_matches;
363 best_size_match = size_match;
364 redump_store_candidate(result, current_game, rom_name, size_text,
365 entry_crc, entry_md5, entry_sha1,
366 size_match, crc32_match, md5_match, sha1_match,
374 result->status = REDUMP_VERIFY_DAT_READ_ERROR;
375 snprintf(result->detail, sizeof(result->detail),
376 "An I/O error occurred while reading the DAT file.");
381 if (result->status == REDUMP_VERIFY_MATCH)
384 result->status = REDUMP_VERIFY_NO_MATCH;
385 if (result->candidate_available) {
386 snprintf(result->detail, sizeof(result->detail),
387 "No exact entry matched; the closest hash-correlated candidate is shown.");
389 snprintf(result->detail, sizeof(result->detail),
390 "No entry matched all required fields and no hash-correlated candidate was found.");