]> FriiDump Source - friidump.git/blob - src/redump_dat.c
028aa1362938c89f25c67aed0b951b2e0b46bca0
[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     char candidate[1024];
55
56     if (!basename || !basename[0] || !out_path || out_path_size == 0)
57         return 0;
58
59     out_path[0] = '\0';
60
61     if (explicit_dir && explicit_dir[0]) {
62         if (!redump_join_path(explicit_dir, NULL, basename,
63                               out_path, out_path_size))
64             return 0;
65         return redump_path_exists(out_path);
66     }
67
68     if (executable_dir && executable_dir[0] &&
69         redump_join_path(executable_dir, "redump_dat", basename,
70                          candidate, sizeof(candidate))) {
71         if (redump_path_exists(candidate)) {
72             snprintf(out_path, out_path_size, "%s", candidate);
73             return 1;
74         }
75
76         /* Retain the executable-relative path as the preferred diagnostic
77          * candidate if neither automatic location exists. */
78         snprintf(out_path, out_path_size, "%s", candidate);
79     }
80
81     if (redump_join_path(".", "redump_dat", basename,
82                          candidate, sizeof(candidate))) {
83         if (redump_path_exists(candidate)) {
84             snprintf(out_path, out_path_size, "%s", candidate);
85             return 1;
86         }
87
88         if (!out_path[0])
89             snprintf(out_path, out_path_size, "%s", candidate);
90     }
91
92     return 0;
93 }
94
95 static int redump_ascii_equal(const char *a, const char *b) {
96     unsigned char ca, cb;
97     if (!a || !b)
98         return 0;
99     while (*a && *b) {
100         ca = (unsigned char)tolower((unsigned char)*a++);
101         cb = (unsigned char)tolower((unsigned char)*b++);
102         if (ca != cb)
103             return 0;
104     }
105     return *a == '\0' && *b == '\0';
106 }
107
108 static int redump_extract_attr(const char *line, const char *name, char *out, size_t out_size) {
109     char pattern[64];
110     const char *start;
111     const char *end;
112     size_t len;
113
114     if (!line || !name || !out || out_size == 0)
115         return 0;
116     snprintf(pattern, sizeof(pattern), "%s=\"", name);
117     start = strstr(line, pattern);
118     if (!start)
119         return 0;
120     start += strlen(pattern);
121     end = strchr(start, '"');
122     if (!end)
123         return 0;
124     len = (size_t)(end - start);
125     if (len >= out_size)
126         len = out_size - 1;
127     memcpy(out, start, len);
128     out[len] = '\0';
129     return 1;
130 }
131
132 static void redump_xml_decode(char *text) {
133     struct entity_s { const char *encoded; const char decoded; };
134     static const struct entity_s entities[] = {
135         {"&amp;", '&'}, {"&quot;", '"'}, {"&apos;", '\''},
136         {"&lt;", '<'}, {"&gt;", '>'}
137     };
138     size_t i;
139     if (!text)
140         return;
141     for (i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) {
142         char *p;
143         size_t encoded_len = strlen(entities[i].encoded);
144         while ((p = strstr(text, entities[i].encoded)) != NULL) {
145             *p = entities[i].decoded;
146             memmove(p + 1, p + encoded_len, strlen(p + encoded_len) + 1);
147         }
148     }
149 }
150
151 static void redump_store_candidate(
152     redump_verify_result *result,
153     const char *game_name,
154     const char *rom_name,
155     const char *size_text,
156     const char *crc32,
157     const char *md5,
158     const char *sha1,
159     int size_match,
160     int crc32_match,
161     int md5_match,
162     int sha1_match,
163     int hash_matches
164 ) {
165     if (!result)
166         return;
167     result->candidate_available = 1;
168     result->candidate_hash_matches = hash_matches;
169     result->size_status = size_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
170     result->crc32_status = crc32_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
171     result->md5_status = md5_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
172     result->sha1_status = sha1_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
173     snprintf(result->game_name, sizeof(result->game_name), "%s",
174              (game_name && game_name[0]) ? game_name : ((rom_name && rom_name[0]) ? rom_name : ""));
175     snprintf(result->rom_name, sizeof(result->rom_name), "%s", rom_name ? rom_name : "");
176     snprintf(result->expected_size, sizeof(result->expected_size), "%s", size_text ? size_text : "");
177     snprintf(result->expected_crc32, sizeof(result->expected_crc32), "%s", crc32 ? crc32 : "");
178     snprintf(result->expected_md5, sizeof(result->expected_md5), "%s", md5 ? md5 : "");
179     snprintf(result->expected_sha1, sizeof(result->expected_sha1), "%s", sha1 ? sha1 : "");
180 }
181
182 void redump_verify_result_init(redump_verify_result *result) {
183     if (!result)
184         return;
185     memset(result, 0, sizeof(*result));
186     result->status = REDUMP_VERIFY_NOT_RUN;
187     result->size_status = REDUMP_FIELD_NOT_AVAILABLE;
188     result->crc32_status = REDUMP_FIELD_NOT_AVAILABLE;
189     result->md5_status = REDUMP_FIELD_NOT_AVAILABLE;
190     result->sha1_status = REDUMP_FIELD_NOT_AVAILABLE;
191 }
192
193 const char *redump_verify_status_string(redump_verify_status status) {
194     switch (status) {
195         case REDUMP_VERIFY_MATCH: return "MATCH";
196         case REDUMP_VERIFY_NO_MATCH: return "NO MATCH";
197         case REDUMP_VERIFY_DAT_NOT_FOUND: return "DAT NOT FOUND";
198         case REDUMP_VERIFY_DAT_READ_ERROR: return "DAT READ ERROR";
199         case REDUMP_VERIFY_INVALID_INPUT: return "INVALID INPUT";
200         default: return "NOT RUN";
201     }
202 }
203
204 const char *redump_field_status_string(redump_field_status status) {
205     switch (status) {
206         case REDUMP_FIELD_PASS: return "PASS";
207         case REDUMP_FIELD_FAIL: return "FAIL";
208         default: return "N/A";
209     }
210 }
211
212 const char *redump_verify_overall_string(const redump_verify_result *result) {
213     if (!result)
214         return "NOT VERIFIED";
215     if (result->status == REDUMP_VERIFY_MATCH)
216         return "VERIFIED AGAINST REDUMP";
217     if (result->status == REDUMP_VERIFY_NO_MATCH)
218         return "NO EXACT MATCH";
219     return "NOT VERIFIED";
220 }
221
222 const char *redump_verify_confidence_string(const redump_verify_result *result) {
223     if (!result)
224         return "Verification unavailable";
225     if (result->status == REDUMP_VERIFY_MATCH)
226         return "Exact archive match";
227     if (result->status == REDUMP_VERIFY_NO_MATCH && result->candidate_available)
228         return "Partial hash correlation; dump not verified";
229     if (result->status == REDUMP_VERIFY_NO_MATCH)
230         return "No archive identity established";
231     if (result->status == REDUMP_VERIFY_INVALID_INPUT)
232         return "Required evidence unavailable";
233     return "Verification unavailable";
234 }
235
236 const char *redump_verify_match_kind_string(const redump_verify_result *result) {
237     if (!result)
238         return "none";
239     if (result->status == REDUMP_VERIFY_MATCH)
240         return "exact";
241     if (result->candidate_available)
242         return "closest";
243     return "none";
244 }
245
246 int redump_verify_dat_file(
247     const char *dat_path,
248     uint64_t image_size,
249     const char *crc32,
250     const char *md5,
251     const char *sha1,
252     redump_verify_result *result
253 ) {
254     FILE *fp;
255     char line[8192];
256     char current_game[512] = "";
257     int best_hash_matches = 0;
258     int best_size_match = 0;
259
260     if (!result)
261         return 0;
262     redump_verify_result_init(result);
263
264     if (dat_path)
265         snprintf(result->dat_path, sizeof(result->dat_path), "%s", dat_path);
266     if (!dat_path || !dat_path[0] || image_size == 0 || !crc32 || !md5 || !sha1 ||
267         !crc32[0] || !md5[0] || !sha1[0]) {
268         result->status = REDUMP_VERIFY_INVALID_INPUT;
269         snprintf(result->detail, sizeof(result->detail),
270                  "Output size plus complete CRC32, MD5, and SHA-1 values are required.");
271         return 0;
272     }
273
274     fp = fopen(dat_path, "rb");
275     if (!fp) {
276         result->status = REDUMP_VERIFY_DAT_NOT_FOUND;
277         snprintf(result->detail, sizeof(result->detail), "Could not open DAT file.");
278         return 0;
279     }
280
281     while (fgets(line, sizeof(line), fp)) {
282         if (strstr(line, "<game ") != NULL) {
283             if (redump_extract_attr(line, "name", current_game, sizeof(current_game)))
284                 redump_xml_decode(current_game);
285             continue;
286         }
287
288         if (strstr(line, "<rom ") != NULL) {
289             char rom_name[512] = "";
290             char size_text[32] = "";
291             char entry_crc[16] = "";
292             char entry_md5[40] = "";
293             char entry_sha1[48] = "";
294             uint64_t entry_size;
295             int size_match;
296             int crc32_match;
297             int md5_match;
298             int sha1_match;
299             int hash_matches;
300
301             if (!redump_extract_attr(line, "size", size_text, sizeof(size_text)) ||
302                 !redump_extract_attr(line, "crc", entry_crc, sizeof(entry_crc)) ||
303                 !redump_extract_attr(line, "md5", entry_md5, sizeof(entry_md5)) ||
304                 !redump_extract_attr(line, "sha1", entry_sha1, sizeof(entry_sha1)))
305                 continue;
306
307             redump_extract_attr(line, "name", rom_name, sizeof(rom_name));
308             redump_xml_decode(rom_name);
309             entry_size = (uint64_t)strtoull(size_text, NULL, 10);
310
311             result->entries_scanned++;
312             size_match = (entry_size == image_size);
313             crc32_match = redump_ascii_equal(crc32, entry_crc);
314             md5_match = redump_ascii_equal(md5, entry_md5);
315             sha1_match = redump_ascii_equal(sha1, entry_sha1);
316             hash_matches = crc32_match + md5_match + sha1_match;
317
318             if (size_match) result->size_matches++;
319             if (crc32_match) result->crc32_matches++;
320             if (md5_match) result->md5_matches++;
321             if (sha1_match) result->sha1_matches++;
322
323             if (size_match && crc32_match && md5_match && sha1_match) {
324                 result->exact_matches++;
325                 if (result->status != REDUMP_VERIFY_MATCH) {
326                     result->status = REDUMP_VERIFY_MATCH;
327                     redump_store_candidate(result, current_game, rom_name, size_text,
328                                            entry_crc, entry_md5, entry_sha1,
329                                            1, 1, 1, 1, 3);
330                     snprintf(result->detail, sizeof(result->detail),
331                              "Size, CRC32, MD5, and SHA-1 matched.");
332                 }
333                 continue;
334             }
335
336             if (result->status != REDUMP_VERIFY_MATCH && hash_matches > 0 &&
337                 (hash_matches > best_hash_matches ||
338                  (hash_matches == best_hash_matches && size_match > best_size_match))) {
339                 best_hash_matches = hash_matches;
340                 best_size_match = size_match;
341                 redump_store_candidate(result, current_game, rom_name, size_text,
342                                        entry_crc, entry_md5, entry_sha1,
343                                        size_match, crc32_match, md5_match, sha1_match,
344                                        hash_matches);
345             }
346         }
347     }
348
349     if (ferror(fp)) {
350         fclose(fp);
351         result->status = REDUMP_VERIFY_DAT_READ_ERROR;
352         snprintf(result->detail, sizeof(result->detail),
353                  "An I/O error occurred while reading the DAT file.");
354         return 0;
355     }
356     fclose(fp);
357
358     if (result->status == REDUMP_VERIFY_MATCH)
359         return 1;
360
361     result->status = REDUMP_VERIFY_NO_MATCH;
362     if (result->candidate_available) {
363         snprintf(result->detail, sizeof(result->detail),
364                  "No exact entry matched; the closest hash-correlated candidate is shown.");
365     } else {
366         snprintf(result->detail, sizeof(result->detail),
367                  "No entry matched all required fields and no hash-correlated candidate was found.");
368     }
369     return 0;
370 }