]> FriiDump Source - friidump.git/blob - src/redump_dat.c
FriiDump 0.5.3.16 candidate: add native reports and Linux Xbox support
[friidump.git] / src / redump_dat.c
1 #include "redump_dat.h"
2
3 #include <ctype.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7
8 static int redump_path_exists(const char *path) {
9     FILE *f;
10     if (!path || !path[0])
11         return 0;
12     f = fopen(path, "rb");
13     if (!f)
14         return 0;
15     fclose(f);
16     return 1;
17 }
18
19 static int redump_join_path(
20     const char *dir,
21     const char *child_dir,
22     const char *basename,
23     char *out_path,
24     size_t out_path_size
25 ) {
26     int written;
27
28     if (!dir || !dir[0] || !basename || !basename[0] ||
29         !out_path || out_path_size == 0)
30         return 0;
31
32     if (child_dir && child_dir[0])
33         written = snprintf(out_path, out_path_size, "%s/%s/%s",
34                            dir, child_dir, basename);
35     else
36         written = snprintf(out_path, out_path_size, "%s/%s",
37                            dir, basename);
38
39     if (written < 0 || (size_t)written >= out_path_size) {
40         out_path[0] = '\0';
41         return 0;
42     }
43
44     return 1;
45 }
46
47 int redump_resolve_dat_path(
48     const char *explicit_dir,
49     const char *executable_dir,
50     const char *basename,
51     char *out_path,
52     size_t out_path_size
53 ) {
54     const char *environment_dir;
55     char candidate[1024];
56
57     if (!basename || !basename[0] || !out_path || out_path_size == 0)
58         return 0;
59
60     out_path[0] = '\0';
61
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))
68             return 0;
69         return redump_path_exists(out_path);
70     }
71
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))
78             return 0;
79         return redump_path_exists(out_path);
80     }
81
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);
88             return 1;
89         }
90         snprintf(out_path, out_path_size, "%s", candidate);
91     }
92
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);
100             return 1;
101         }
102     }
103
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);
109             return 1;
110         }
111         if (!out_path[0])
112             snprintf(out_path, out_path_size, "%s", candidate);
113     }
114
115     return 0;
116 }
117
118 static int redump_ascii_equal(const char *a, const char *b) {
119     unsigned char ca, cb;
120     if (!a || !b)
121         return 0;
122     while (*a && *b) {
123         ca = (unsigned char)tolower((unsigned char)*a++);
124         cb = (unsigned char)tolower((unsigned char)*b++);
125         if (ca != cb)
126             return 0;
127     }
128     return *a == '\0' && *b == '\0';
129 }
130
131 static int redump_extract_attr(const char *line, const char *name, char *out, size_t out_size) {
132     char pattern[64];
133     const char *start;
134     const char *end;
135     size_t len;
136
137     if (!line || !name || !out || out_size == 0)
138         return 0;
139     snprintf(pattern, sizeof(pattern), "%s=\"", name);
140     start = strstr(line, pattern);
141     if (!start)
142         return 0;
143     start += strlen(pattern);
144     end = strchr(start, '"');
145     if (!end)
146         return 0;
147     len = (size_t)(end - start);
148     if (len >= out_size)
149         len = out_size - 1;
150     memcpy(out, start, len);
151     out[len] = '\0';
152     return 1;
153 }
154
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         {"&amp;", '&'}, {"&quot;", '"'}, {"&apos;", '\''},
159         {"&lt;", '<'}, {"&gt;", '>'}
160     };
161     size_t i;
162     if (!text)
163         return;
164     for (i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) {
165         char *p;
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);
170         }
171     }
172 }
173
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,
179     const char *crc32,
180     const char *md5,
181     const char *sha1,
182     int size_match,
183     int crc32_match,
184     int md5_match,
185     int sha1_match,
186     int hash_matches
187 ) {
188     if (!result)
189         return;
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 : "");
203 }
204
205 void redump_verify_result_init(redump_verify_result *result) {
206     if (!result)
207         return;
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;
214 }
215
216 const char *redump_verify_status_string(redump_verify_status status) {
217     switch (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";
224     }
225 }
226
227 const char *redump_field_status_string(redump_field_status status) {
228     switch (status) {
229         case REDUMP_FIELD_PASS: return "PASS";
230         case REDUMP_FIELD_FAIL: return "FAIL";
231         default: return "N/A";
232     }
233 }
234
235 const char *redump_verify_overall_string(const redump_verify_result *result) {
236     if (!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";
243 }
244
245 const char *redump_verify_confidence_string(const redump_verify_result *result) {
246     if (!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";
257 }
258
259 const char *redump_verify_match_kind_string(const redump_verify_result *result) {
260     if (!result)
261         return "none";
262     if (result->status == REDUMP_VERIFY_MATCH)
263         return "exact";
264     if (result->candidate_available)
265         return "closest";
266     return "none";
267 }
268
269 int redump_verify_dat_file(
270     const char *dat_path,
271     uint64_t image_size,
272     const char *crc32,
273     const char *md5,
274     const char *sha1,
275     redump_verify_result *result
276 ) {
277     FILE *fp;
278     char line[8192];
279     char current_game[512] = "";
280     int best_hash_matches = 0;
281     int best_size_match = 0;
282
283     if (!result)
284         return 0;
285     redump_verify_result_init(result);
286
287     if (dat_path)
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.");
294         return 0;
295     }
296
297     fp = fopen(dat_path, "rb");
298     if (!fp) {
299         result->status = REDUMP_VERIFY_DAT_NOT_FOUND;
300         snprintf(result->detail, sizeof(result->detail), "Could not open DAT file.");
301         return 0;
302     }
303
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);
308             continue;
309         }
310
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] = "";
317             uint64_t entry_size;
318             int size_match;
319             int crc32_match;
320             int md5_match;
321             int sha1_match;
322             int hash_matches;
323
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)))
328                 continue;
329
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);
333
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;
340
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++;
345
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,
352                                            1, 1, 1, 1, 3);
353                     snprintf(result->detail, sizeof(result->detail),
354                              "Size, CRC32, MD5, and SHA-1 matched.");
355                 }
356                 continue;
357             }
358
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,
367                                        hash_matches);
368             }
369         }
370     }
371
372     if (ferror(fp)) {
373         fclose(fp);
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.");
377         return 0;
378     }
379     fclose(fp);
380
381     if (result->status == REDUMP_VERIFY_MATCH)
382         return 1;
383
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.");
388     } else {
389         snprintf(result->detail, sizeof(result->detail),
390                  "No entry matched all required fields and no hash-correlated candidate was found.");
391     }
392     return 0;
393 }