102 Patches: Detours to the Rescue
C reference for DttR maintainers and modders.
Loading...
Searching...
No Matches
iso.c
Go to the documentation of this file.
1#include <dttr_iso.h>
2#include <dttr_path.h>
3
4#include <physfs.h>
5#include <sds.h>
6
7#include <errno.h>
8#include <stdio.h>
9#include <string.h>
10
11#include <direct.h>
12#include <windows.h>
13
14#define MKDIR(path) \
15 (CreateDirectoryA((path), NULL) || GetLastError() == ERROR_ALREADY_EXISTS)
16
17enum { ISO_SECTOR_SIZE = 2048 };
18
19static int physfs_refcount;
20static char last_error[256];
21
22// Records the latest ISO-layer failure for launcher errors.
23static void set_error(const char *message) {
24 if (!message) {
25 message = "unknown error";
26 }
27
28 strncpy(last_error, message, sizeof(last_error) - 1);
29 last_error[sizeof(last_error) - 1] = '\0';
30}
31
32// Preserves the PhysicsFS failure text with the operation that triggered it.
33static void set_physfs_error(const char *context) {
34 const char *err = PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode());
35
36 if (!err) {
37 err = "unknown PhysicsFS error";
38 }
39
40 snprintf(last_error, sizeof(last_error), "%s: %s", context, err);
41}
42
43// Returns the current ISO error or a stable default before the first failure.
44const char *DTTR_ISO_LastError() {
45 return last_error[0] ? last_error : "no error";
46}
47
48static bool is_iso_version_suffix(const char *suffix) {
49 if (!suffix || suffix[0] != ';' || !suffix[1]) {
50 return false;
51 }
52
53 for (const char *ch = suffix + 1; *ch; ch++) {
54 if (*ch < '0' || *ch > '9') {
55 return false;
56 }
57 }
58
59 return true;
60}
61
62// Returns the segment length without a trailing ISO9660 version suffix.
63static size_t strip_iso_version_suffix_len(const char *segment, size_t segment_len) {
64 for (size_t i = segment_len; i > 1; i--) {
65 if (segment[i - 1] == ';' && is_iso_version_suffix(segment + i - 1)) {
66 return i - 1;
67 }
68 }
69
70 return segment_len;
71}
72
73// Normalizes the cache root before appending ISO-relative path segments.
74static void trim_trailing_separators(sds path) {
75 while (sdslen(path) > 0 && DTTR_Path_IsSeparator(path[sdslen(path) - 1])) {
76 sdsrange(path, 0, -2);
77 }
78}
79
80// Appends one lower-case cache path segment without an ISO version suffix.
81static bool sdscat_lower_segment(sds *out, const char *segment, size_t segment_len) {
82 segment_len = strip_iso_version_suffix_len(segment, segment_len);
83
84 for (size_t i = 0; i < segment_len; i++) {
85 const char ch = DTTR_Path_AsciiLower(segment[i]);
86
87 if (!DTTR_Path_AppendChar(out, ch)) {
88 return false;
89 }
90 }
91
92 return true;
93}
94
95// Converts an ISO-relative file path into its deterministic cache path.
97 const char *cache_root,
98 const char *iso_relative_path,
99 char *out_path,
100 size_t out_path_size
101) {
102 if (!cache_root || !cache_root[0] || !iso_relative_path || !iso_relative_path[0]
103 || !out_path || out_path_size == 0) {
104 return false;
105 }
106
107 sds path = sdsnew(cache_root);
108
109 if (!path) {
110 return false;
111 }
112
114
115 const char *p = DTTR_Path_SkipSeparators(iso_relative_path);
116
117 bool wrote_segment = false;
118 bool ok = true;
119
120 while (*p) {
121 const char *segment = p;
122 size_t segment_len = DTTR_Path_SegmentLen(p);
123
124 if (DTTR_Path_IsRelativeSegment(segment, segment_len)) {
125 ok = false;
126 break;
127 }
128
129 if (!DTTR_Path_AppendSeparator(&path, '\\')
130 || !sdscat_lower_segment(&path, segment, segment_len)) {
131 ok = false;
132 break;
133 }
134
135 wrote_segment = true;
136
137 p += segment_len;
138
139 if (DTTR_Path_IsSeparator(*p)) {
141
142 if (!*p) {
143 ok = false;
144 break;
145 }
146 }
147 }
148
149 ok = ok && wrote_segment && DTTR_Path_CopySds(out_path, out_path_size, path);
150 sdsfree(path);
151 return ok;
152}
153
154// Starts PhysicsFS once for ISO access and lets nested callers share the session.
155static bool physfs_init() {
156 if (physfs_refcount > 0) {
158 return true;
159 }
160
161 if (!PHYSFS_init("dttr")) {
162 set_physfs_error("PHYSFS_init failed");
163 return false;
164 }
165
166 physfs_refcount = 1;
167
168 return true;
169}
170
171// Drops one ISO PhysicsFS reference and shuts down after the last image closes.
172static void physfs_deinit() {
173 if (physfs_refcount <= 0) {
174 return;
175 }
176
178
179 if (physfs_refcount == 0) {
180 PHYSFS_deinit();
181 }
182}
183
184// Mounts an ISO image through PhysicsFS for case-insensitive extraction.
185bool DTTR_ISO_Open(DTTR_IsoImage *iso, const char *iso_path) {
186 if (!iso || !iso_path || !iso_path[0] || strlen(iso_path) >= sizeof(iso->iso_path)) {
187 set_error("invalid ISO path");
188 return false;
189 }
190
191 memset(iso, 0, sizeof(*iso));
192
193 if (!physfs_init()) {
194 return false;
195 }
196
197 if (!PHYSFS_mount(iso_path, NULL, 1)) {
198 set_physfs_error("PHYSFS_mount failed");
200 return false;
201 }
202
203 DTTR_Path_CopyString(iso->iso_path, sizeof(iso->iso_path), iso_path);
204 iso->open = true;
205
206 return true;
207}
208
209// Matches a requested path segment against an ISO directory entry.
211 const char *name,
212 const char *segment,
213 size_t segment_len
214) {
215 const size_t name_len = strlen(name);
216 return (name_len == segment_len
217 || (name_len > segment_len && is_iso_version_suffix(name + segment_len)))
218 && DTTR_Path_AsciiIeqN(name, segment, segment_len);
219}
220
221// Enumerates an ISO directory to recover stored casing for one segment.
222static bool find_case_match(
223 const char *parent,
224 const char *segment,
225 size_t segment_len,
226 char *out_name,
227 size_t out_name_size
228) {
229 char **entries = PHYSFS_enumerateFiles(parent && parent[0] ? parent : "");
230
231 if (!entries) {
232 return false;
233 }
234
235 bool found = false;
236
237 for (char **entry = entries; *entry; entry++) {
238 if (!name_matches_segment(*entry, segment, segment_len)) {
239 continue;
240 }
241
242 if (!DTTR_Path_CopyString(out_name, out_name_size, *entry)) {
243 continue;
244 }
245
246 found = true;
247 break;
248 }
249
250 PHYSFS_freeList(entries);
251 return found;
252}
253
254// Resolves ISO-relative paths to exact PhysicsFS entry names.
256 const char *requested,
257 char *out_path,
258 size_t out_path_size
259) {
260 if (!requested || !requested[0] || !out_path || out_path_size == 0) {
261 return false;
262 }
263
264 sds path = sdsempty();
265
266 if (!path) {
267 return false;
268 }
269
270 const char *p = DTTR_Path_SkipSeparators(requested);
271
272 bool wrote_segment = false;
273 bool ok = true;
274
275 while (*p) {
276 const char *segment = p;
277 size_t segment_len = DTTR_Path_SegmentLen(p);
278
279 if (DTTR_Path_IsRelativeSegment(segment, segment_len)) {
280 ok = false;
281 break;
282 }
283
284 char match[DTTR_ISO_MAX_PATH];
285
286 if (!find_case_match(path, segment, segment_len, match, sizeof(match))) {
287 ok = false;
288 break;
289 }
290
291 if (!DTTR_Path_AppendSegment(&path, match, '/')) {
292 ok = false;
293 break;
294 }
295
296 wrote_segment = true;
297
298 p += segment_len;
300 }
301
302 ok = ok && wrote_segment && DTTR_Path_CopySds(out_path, out_path_size, path);
303 sdsfree(path);
304 return ok;
305}
306
307// Creates the cache directory chain for an extracted ISO file.
308static bool create_parent_dirs(const char *path) {
309 char tmp[DTTR_ISO_MAX_PATH];
310
311 const size_t len = strlen(path);
312
313 if (len >= sizeof(tmp)) {
314 return false;
315 }
316
317 memcpy(tmp, path, len + 1);
318
319 for (size_t i = 1; tmp[i]; i++) {
320 if (!DTTR_Path_IsSeparator(tmp[i])) {
321 continue;
322 }
323
324 if (i == 2 && tmp[1] == ':') {
325 continue;
326 }
327
328 char saved = tmp[i];
329 tmp[i] = '\0';
330
331 if (tmp[0]) {
332 MKDIR(tmp);
333 }
334
335 tmp[i] = saved;
336 }
337
338 return true;
339}
340
341// Reuse cached files only when size and bytes match the ISO.
343 const char *path,
344 PHYSFS_File *physfs_file,
345 size_t size
346) {
347 FILE *file = fopen(path, "rb");
348
349 if (!file) {
350 return false;
351 }
352
353 bool matches = false;
354 if (fseek(file, 0, SEEK_END) != 0) {
355 goto done;
356 }
357
358 const long existing = ftell(file);
359 if (existing < 0 || (size_t)existing != size || fseek(file, 0, SEEK_SET) != 0) {
360 goto done;
361 }
362
363 if (!PHYSFS_seek(physfs_file, 0)) {
364 goto done;
365 }
366
367 char disk_buffer[ISO_SECTOR_SIZE];
368 char iso_buffer[ISO_SECTOR_SIZE];
369 size_t remaining = size;
370 matches = true;
371 while (remaining > 0) {
372 const size_t chunk = remaining < sizeof(disk_buffer) ? remaining
373 : sizeof(disk_buffer);
374 if (fread(disk_buffer, 1, chunk, file) != chunk
375 || PHYSFS_readBytes(physfs_file, iso_buffer, chunk) != (PHYSFS_sint64)chunk
376 || memcmp(disk_buffer, iso_buffer, chunk) != 0) {
377 matches = false;
378 break;
379 }
380
381 remaining -= chunk;
382 }
383
384done:
385 fclose(file);
386 if (!PHYSFS_seek(physfs_file, 0)) {
387 set_physfs_error("PHYSFS_seek failed");
388 return false;
389 }
390
391 return matches;
392}
393
394// Extends a resolved ISO directory path with one child entry.
395static sds child_iso_path(const char *parent, const char *entry) {
396 sds child = sdsnew(parent);
397
398 if (!child) {
399 return NULL;
400 }
401
402 if (!DTTR_Path_AppendSegment(&child, entry, '/')) {
403 sdsfree(child);
404 return NULL;
405 }
406
407 return child;
408}
409
410// Copies one resolved ISO file into the launcher cache.
412 DTTR_IsoImage *iso,
413 const char *iso_relative_path,
414 const char *cache_root,
415 char *out_path,
416 size_t out_path_size
417) {
418 if (!iso || !iso->open) {
419 set_error("ISO is not open");
420 return false;
421 }
422
424 cache_root,
425 iso_relative_path,
426 out_path,
427 out_path_size
428 )) {
429 set_error("invalid ISO cache path");
430 return false;
431 }
432
433 char physfs_path[DTTR_ISO_MAX_PATH];
434
435 if (!resolve_iso_path_case(iso_relative_path, physfs_path, sizeof(physfs_path))) {
436 set_error("file not found in ISO");
437 return false;
438 }
439
440 PHYSFS_File *in = PHYSFS_openRead(physfs_path);
441
442 if (!in) {
443 set_physfs_error("PHYSFS_openRead failed");
444 return false;
445 }
446
447 const PHYSFS_sint64 length = PHYSFS_fileLength(in);
448
449 if (length < 0) {
450 set_physfs_error("PHYSFS_fileLength failed");
451 PHYSFS_close(in);
452 return false;
453 }
454
455 if ((uint64_t)length > (uint64_t)SIZE_MAX) {
456 set_error("ISO file is too large to cache");
457 PHYSFS_close(in);
458 return false;
459 }
460
461 if (file_matches_physfs_file(out_path, in, (size_t)length)) {
462 PHYSFS_close(in);
463 return true;
464 }
465
466 if (!PHYSFS_seek(in, 0)) {
467 set_physfs_error("PHYSFS_seek failed");
468 PHYSFS_close(in);
469 return false;
470 }
471
472 if (!create_parent_dirs(out_path)) {
473 set_error("could not create cache directories");
474 PHYSFS_close(in);
475 return false;
476 }
477
478 FILE *out = fopen(out_path, "wb");
479
480 if (!out) {
481 set_error("could not open cache output file");
482 PHYSFS_close(in);
483 return false;
484 }
485
486 char buffer[ISO_SECTOR_SIZE];
487 PHYSFS_sint64 remaining = length;
488
489 while (remaining > 0) {
490 const PHYSFS_uint64 chunk = remaining < (PHYSFS_sint64)sizeof(buffer)
491 ? (PHYSFS_uint64)remaining
492 : (PHYSFS_uint64)sizeof(buffer);
493 const PHYSFS_sint64 got = PHYSFS_readBytes(in, buffer, chunk);
494
495 if (got != (PHYSFS_sint64)chunk
496 || fwrite(buffer, 1, (size_t)chunk, out) != chunk) {
497 set_error("could not extract ISO file");
498 fclose(out);
499 PHYSFS_close(in);
500 return false;
501 }
502
503 remaining -= got;
504 }
505
506 const bool output_closed = fclose(out) == 0;
507 const bool input_closed = PHYSFS_close(in) != 0;
508 if (!output_closed || !input_closed) {
509 set_error("could not finish ISO extraction");
510 return false;
511 }
512
513 return true;
514}
515
516// Walks a resolved ISO directory and extracts every regular file into the cache tree.
518 DTTR_IsoImage *iso,
519 const char *physfs_path,
520 const char *cache_root
521) {
522 char **entries = PHYSFS_enumerateFiles(physfs_path);
523
524 if (!entries) {
525 set_physfs_error("PHYSFS_enumerateFiles failed");
526 return false;
527 }
528
529 bool ok = true;
530
531 for (char **entry = entries; ok && *entry; entry++) {
532 sds child = child_iso_path(physfs_path, *entry);
533
534 if (!child) {
535 set_error("could not build ISO tree path");
536 ok = false;
537 break;
538 }
539
540 PHYSFS_Stat stat;
541
542 if (!PHYSFS_stat(child, &stat)) {
543 set_physfs_error("PHYSFS_stat failed");
544 ok = false;
545 } else if (stat.filetype == PHYSFS_FILETYPE_DIRECTORY) {
546 ok = extract_tree_path(iso, child, cache_root);
547 } else if (stat.filetype == PHYSFS_FILETYPE_REGULAR) {
548 char out_path[DTTR_ISO_MAX_PATH];
549 ok = DTTR_ISO_ExtractFile(iso, child, cache_root, out_path, sizeof(out_path));
550 }
551
552 sdsfree(child);
553 }
554
555 PHYSFS_freeList(entries);
556 return ok;
557}
558
559// Resolves and extracts an ISO directory tree used by the launcher cache.
561 DTTR_IsoImage *iso,
562 const char *iso_relative_path,
563 const char *cache_root
564) {
565 if (!iso || !iso->open) {
566 set_error("ISO is not open");
567 return false;
568 }
569
570 char physfs_path[DTTR_ISO_MAX_PATH];
571
572 if (!resolve_iso_path_case(iso_relative_path, physfs_path, sizeof(physfs_path))) {
573 set_error("directory not found in ISO");
574 return false;
575 }
576
577 PHYSFS_Stat stat;
578
579 if (!PHYSFS_stat(physfs_path, &stat)) {
580 set_physfs_error("PHYSFS_stat failed");
581 return false;
582 }
583
584 if (stat.filetype != PHYSFS_FILETYPE_DIRECTORY) {
585 set_error("ISO path is not a directory");
586 return false;
587 }
588
589 return extract_tree_path(iso, physfs_path, cache_root);
590}
591
592// Unmounts the ISO image and releases the shared PhysicsFS reference owned by it.
594 if (!iso || !iso->open) {
595 return;
596 }
597
598 PHYSFS_unmount(iso->iso_path);
600 memset(iso, 0, sizeof(*iso));
601}
const DWORD size
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
#define DTTR_ISO_MAX_PATH
Definition dttr_iso.h:7
bool DTTR_Path_AsciiIeqN(const char *lhs, const char *rhs, size_t n)
Definition path.c:58
bool DTTR_Path_IsSeparator(char ch)
Definition path.c:76
const char * DTTR_Path_SkipSeparators(const char *path)
Definition path.c:80
bool DTTR_Path_IsRelativeSegment(const char *segment, size_t segment_len)
Definition path.c:98
bool DTTR_Path_CopyString(char *out, size_t out_size, const char *value)
Definition path.c:68
bool DTTR_Path_CopySds(char *out, size_t out_size, sds value)
Definition path.c:72
bool DTTR_Path_AppendChar(sds *path, char ch)
Definition path.c:262
bool DTTR_Path_AppendSeparator(sds *path, char separator)
Definition path.c:272
bool DTTR_Path_AppendSegment(sds *path, const char *segment, char separator)
Definition path.c:276
char DTTR_Path_AsciiLower(char ch)
Definition path.c:50
size_t DTTR_Path_SegmentLen(const char *path)
Definition path.c:88
static bool physfs_init()
Definition iso.c:155
static size_t strip_iso_version_suffix_len(const char *segment, size_t segment_len)
Definition iso.c:63
static void set_error(const char *message)
Definition iso.c:23
#define MKDIR(path)
Definition iso.c:14
static void trim_trailing_separators(sds path)
Definition iso.c:74
bool DTTR_ISO_CachePathForFile(const char *cache_root, const char *iso_relative_path, char *out_path, size_t out_path_size)
Definition iso.c:96
static bool is_iso_version_suffix(const char *suffix)
Definition iso.c:48
static bool extract_tree_path(DTTR_IsoImage *iso, const char *physfs_path, const char *cache_root)
Definition iso.c:517
static bool resolve_iso_path_case(const char *requested, char *out_path, size_t out_path_size)
Definition iso.c:255
bool DTTR_ISO_ExtractFile(DTTR_IsoImage *iso, const char *iso_relative_path, const char *cache_root, char *out_path, size_t out_path_size)
Definition iso.c:411
void DTTR_ISO_Close(DTTR_IsoImage *iso)
Definition iso.c:593
static sds child_iso_path(const char *parent, const char *entry)
Definition iso.c:395
@ ISO_SECTOR_SIZE
Definition iso.c:17
static void physfs_deinit()
Definition iso.c:172
static bool sdscat_lower_segment(sds *out, const char *segment, size_t segment_len)
Definition iso.c:81
bool DTTR_ISO_Open(DTTR_IsoImage *iso, const char *iso_path)
Definition iso.c:185
static int physfs_refcount
Definition iso.c:19
static bool file_matches_physfs_file(const char *path, PHYSFS_File *physfs_file, size_t size)
Definition iso.c:342
static bool find_case_match(const char *parent, const char *segment, size_t segment_len, char *out_name, size_t out_name_size)
Definition iso.c:222
bool DTTR_ISO_ExtractTree(DTTR_IsoImage *iso, const char *iso_relative_path, const char *cache_root)
Definition iso.c:560
static bool name_matches_segment(const char *name, const char *segment, size_t segment_len)
Definition iso.c:210
static void set_physfs_error(const char *context)
Definition iso.c:33
const char * DTTR_ISO_LastError()
Definition iso.c:44
static bool create_parent_dirs(const char *path)
Definition iso.c:308
static char last_error[256]
Definition iso.c:20
char iso_path[DTTR_ISO_MAX_PATH]
Definition dttr_iso.h:11