]> git.xonotic.org Git - xonotic/gmqcc.git/blob - pak.c
ba820ef2f98b9853a47d3a1c4fc81084bee1279a
[xonotic/gmqcc.git] / pak.c
1 /*
2  * Copyright (C) 2013
3  *     Dale Weiler
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a copy of
6  * this software and associated documentation files (the "Software"), to deal in
7  * the Software without restriction, including without limitation the rights to
8  * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9  * of the Software, and to permit persons to whom the Software is furnished to do
10  * so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in all
13  * copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21  * SOFTWARE.
22  */
23 #include <string.h>
24 #include <stdlib.h>
25
26 #include "gmqcc.h"
27 #include "platform.h"
28
29 /*
30  * The PAK format uses a FOURCC concept for storing the magic ident within
31  * the header as a uint32_t.
32  */
33 #define PAK_FOURCC ((uint32_t)(((uint8_t)'P'|((uint8_t)'A'<<8)|((uint8_t)'C'<<16)|((uint8_t)'K'<<24))))
34
35 typedef struct {
36     uint32_t magic;  /* "PACK" */
37
38     /*
39      * Offset to first directory entry in PAK file.  It's often
40      * best to store the directories at the end of the file opposed
41      * to the front, since it allows easy insertion without having
42      * to load the entire file into memory again.
43      */
44     uint32_t diroff;
45     uint32_t dirlen;
46 } pak_header_t;
47
48 /*
49  * A directory, is sort of a "file entry".  The concept of
50  * a directory in Quake world is a "file entry/record". This
51  * describes a file (with directories/nested ones too in it's
52  * file name).  Hence it can be a file, file with directory, or
53  * file with directories.
54  */
55 typedef struct {
56     char     name[56];
57     uint32_t pos;
58     uint32_t len;
59 } pak_directory_t;
60
61 /*
62  * Used to get the next token from a string, where the
63  * strings themselfs are seperated by chracters from
64  * `sep`.  This is essentially strsep.
65  */
66 static char *pak_tree_sep(char **str, const char *sep) {
67     char *beg = *str;
68     char *end;
69
70     if (!beg)
71         return NULL;
72
73     if (*(end = beg + strcspn(beg, sep)))
74         * end++ = '\0'; /* null terminate */
75     else
76           end   = 0;
77
78     *str = end;
79     return beg;
80 }
81
82 /*
83  * When given a string like "a/b/c/d/e/file"
84  * this function will handle the creation of
85  * the directory structure, included nested
86  * directories.
87  */
88 static void pak_tree_build(const char *entry) {
89     char *directory;
90     char *elements[28];
91     char *pathsplit;
92     char *token;
93
94     size_t itr;
95     size_t jtr;
96
97     pathsplit = (char *)mem_a(56);
98     directory = (char *)mem_a(56);
99
100     memset(pathsplit, 0, 56);
101
102     platform_strncpy(directory, entry, 56);
103     for (itr = 0; (token = pak_tree_sep(&directory, "/")) != NULL; itr++) {
104         elements[itr] = token;
105     }
106
107     for (jtr = 0; jtr < itr - 1; jtr++) {
108         platform_strcat(pathsplit, elements[jtr]);
109         platform_strcat(pathsplit, "/");
110
111         if (fs_dir_make(pathsplit)) {
112             mem_d(pathsplit);
113             mem_d(directory);
114
115             /* TODO: undo on fail */
116
117             return;
118         }
119     }
120
121     mem_d(pathsplit);
122     mem_d(directory);
123 }
124
125 typedef struct {
126     pak_directory_t *directories;
127     pak_header_t     header;
128     FILE            *handle;
129     bool             insert;
130 } pak_file_t;
131
132 static pak_file_t *pak_open_read(const char *file) {
133     pak_file_t *pak;
134     size_t      itr;
135
136     if (!(pak = (pak_file_t*)mem_a(sizeof(pak_file_t))))
137         return NULL;
138
139     if (!(pak->handle = fs_file_open(file, "rb"))) {
140         mem_d(pak);
141         return NULL;
142     }
143
144     pak->directories = NULL;
145     pak->insert      = false; /* read doesn't allow insert */
146
147     memset         (&pak->header, 0, sizeof(pak_header_t));
148     fs_file_read   (&pak->header,    sizeof(pak_header_t), 1, pak->handle);
149     util_endianswap(&pak->header, 1, sizeof(pak_header_t));
150
151     /*
152      * Every PAK file has "PACK" stored as FOURCC data in the
153      * header.  If this data cannot compare (as checked here), it's
154      * probably not a PAK file.
155      */
156     if (pak->header.magic != PAK_FOURCC) {
157         fs_file_close(pak->handle);
158         mem_d        (pak);
159         return NULL;
160     }
161
162     /*
163      * Time to read in the directory handles and prepare the directories
164      * vector.  We're going to be reading some the file inwards soon.
165      */
166     fs_file_seek(pak->handle, pak->header.diroff, SEEK_SET);
167
168     /*
169      * Read in all directories from the PAK file. These are considered
170      * to be the "file entries".
171      */
172     for (itr = 0; itr < pak->header.dirlen / 64; itr++) {
173         pak_directory_t dir;
174         fs_file_read   (&dir,    sizeof(pak_directory_t), 1, pak->handle);
175         util_endianswap(&dir, 1, sizeof(pak_directory_t));
176
177         vec_push(pak->directories, dir);
178     }
179     return pak;
180 }
181
182 static pak_file_t *pak_open_write(const char *file) {
183     pak_file_t *pak;
184
185     if (!(pak = (pak_file_t*)mem_a(sizeof(pak_file_t))))
186         return NULL;
187
188     /*
189      * Generate the required directory structure / tree for
190      * writing this PAK file too.
191      */
192     pak_tree_build(file);
193
194     if (!(pak->handle = fs_file_open(file, "wb"))) {
195         /*
196          * The directory tree that was created, needs to be
197          * removed entierly if we failed to open a file.
198          */
199         /* TODO backup directory clean */
200
201         mem_d(pak);
202         return NULL;
203     }
204
205     memset(&(pak->header), 0, sizeof(pak_header_t));
206
207     /*
208      * We're in "insert" mode, we need to do things like header
209      * "patching" and writing the directories at the end of the
210      * file.
211      */
212     pak->insert       = true;
213     pak->header.magic = PAK_FOURCC;
214
215     /* on BE systems we need to swap the byte order of the FOURCC */
216     util_endianswap(&pak->header.magic, 1, sizeof(uint32_t));
217
218     /*
219      * We need to write out the header since files will be wrote out to
220      * this even with directory entries, and that not wrote.  The header
221      * will need to be patched in later with a file_seek, and overwrite,
222      * we could use offsets and other trickery.  This is just easier.
223      */
224     fs_file_write(&(pak->header), sizeof(pak_header_t), 1, pak->handle);
225
226     return pak;
227 }
228
229 static pak_file_t *pak_open(const char *file, const char *mode) {
230     if (!file || !mode)
231         return NULL;
232
233     switch (*mode) {
234         case 'r': return pak_open_read (file);
235         case 'w': return pak_open_write(file);
236     }
237
238     return NULL;
239 }
240
241 static bool pak_exists(pak_file_t *pak, const char *file, pak_directory_t **dir) {
242     size_t itr;
243
244     if (!pak || !file)
245         return false;
246
247     for (itr = 0; itr < vec_size(pak->directories); itr++) {
248         if (!strcmp(pak->directories[itr].name, file)) {
249             /*
250              * Store back a pointer to the directory that matches
251              * the request if requested (NULL is not allowed).
252              */
253             if (dir) {
254                 *dir = &(pak->directories[itr]);
255             }
256             return true;
257         }
258     }
259
260     return false;
261 }
262
263 /*
264  * Extraction abilities.  These work as you expect them to.
265  */
266 static bool pak_extract_one(pak_file_t *pak, const char *file, const char *outdir) {
267     pak_directory_t *dir   = NULL;
268     unsigned char   *dat   = NULL;
269     char            *local = NULL;
270     FILE            *out   = NULL;
271
272     if (!pak_exists(pak, file, &dir)) {
273         return false;
274     }
275
276     if (!(dat = (unsigned char *)mem_a(dir->len)))
277         goto err;
278
279     /*
280      * Generate the directory structure / tree that will be required
281      * to store the extracted file.
282      */
283     pak_tree_build(file);
284
285     /* TODO portable path seperators */
286     util_asprintf(&local, "%s/%s", outdir, file);
287
288     /*
289      * Now create the file, if this operation fails.  Then abort
290      * It shouldn't fail though.
291      */
292     if (!(out = fs_file_open(local, "wb")))
293         goto err;
294
295     /* free memory for directory string */
296     mem_d(local);
297
298     /* read */
299     if (fs_file_seek (pak->handle, dir->pos, SEEK_SET) != 0)
300         goto err;
301
302     fs_file_read (dat, 1, dir->len, pak->handle);
303     fs_file_write(dat, 1, dir->len, out);
304     fs_file_close(out);
305
306     mem_d(dat);
307     return true;
308
309 err:
310     if (dat) mem_d(dat);
311     if (out) fs_file_close(out);
312     return false;
313 }
314
315 static bool pak_extract_all(pak_file_t *pak, const char *dir) {
316     size_t itr;
317
318     if (!fs_dir_make(dir))
319         return false;
320
321     for (itr = 0; itr < vec_size(pak->directories); itr++) {
322         if (!pak_extract_one(pak, pak->directories[itr].name, dir))
323             return false;
324     }
325
326     return true;
327 }
328
329 /*
330  * Insertion functions (the opposite of extraction).  Yes for generating
331  * PAKs.
332  */
333 static bool pak_insert_one(pak_file_t *pak, const char *file) {
334     pak_directory_t dir;
335     unsigned char  *dat;
336     long            len;
337     FILE           *fp;
338
339     /*
340      * We don't allow insertion on files that already exist within the
341      * pak file.  Weird shit can happen if we allow that ;). We also
342      * don't allow insertion if the pak isn't opened in write mode.
343      */
344     if (!pak || !file || !pak->insert || pak_exists(pak, file, NULL))
345         return false;
346
347     if (!(fp = fs_file_open(file, "rb")))
348         return false;
349
350     /*
351      * Calculate the total file length, since it will be wrote to
352      * the directory entry, and the actual contents of the file
353      * to the PAK file itself.
354      */
355     if (fs_file_seek(fp, 0, SEEK_END) != 0 || ((len = fs_file_tell(fp)) < 0))
356         goto err;
357     if (fs_file_seek(fp, 0, SEEK_SET) != 0)
358         goto err;
359
360     dir.len = len;
361     dir.pos = fs_file_tell(pak->handle);
362
363     /*
364      * We're limited to 56 bytes for a file name string, that INCLUDES
365      * the directory and '/' seperators.
366      */
367     if (strlen(file) >= 56)
368         goto err;
369
370     platform_strncpy(dir.name, file, strlen(file));
371
372     /*
373      * Allocate some memory for loading in the data that will be
374      * redirected into the PAK file.
375      */
376     if (!(dat = (unsigned char *)mem_a(dir.len)))
377         goto err;
378
379     fs_file_read (dat, dir.len, 1, fp);
380     fs_file_close(fp);
381     fs_file_write(dat, dir.len, 1, pak->handle);
382
383     /*
384      * Now add the directory to the directories vector, so pak_close
385      * can actually write it.
386      */
387     vec_push(pak->directories, dir);
388
389     return true;
390
391 err:
392     fs_file_close(fp);
393     return false;
394 }
395
396 /*
397  * Like pak_insert_one, except this collects files in all directories
398  * from a root directory, and inserts them all.
399  */
400 #if 0
401 static bool pak_insert_all(pak_file_t *pak, const char *dir) {
402     DIR           *dp;
403     struct dirent *dirp;
404
405     if (!(pak->insert))
406         return false;
407
408     if (!(dp = fs_dir_open(dir)))
409         return false;
410
411     while ((dirp = fs_dir_read(dp))) {
412         if (!(pak_insert_one(pak, dirp->d_name))) {
413             fs_dir_close(dp);
414             return false;
415         }
416     }
417
418     fs_dir_close(dp);
419     return true;
420 }
421 #endif /*!if 0 renable when ready to use */
422
423 static bool pak_close(pak_file_t *pak) {
424     size_t itr;
425     long   tell;
426
427     if (!pak)
428         return false;
429
430     /*
431      * In insert mode we need to patch the header, and write
432      * our directory entries at the end of the file.
433      */
434     if (pak->insert) {
435         if ((tell = fs_file_tell(pak->handle)) != 0)
436             goto err;
437
438         pak->header.dirlen = vec_size(pak->directories) * 64;
439         pak->header.diroff = tell;
440
441         /* patch header */
442         if (fs_file_seek (pak->handle, 0, SEEK_SET) != 0)
443             goto err;
444
445         fs_file_write(&(pak->header), sizeof(pak_header_t), 1, pak->handle);
446
447         /* write directories */
448         if (fs_file_seek (pak->handle, pak->header.diroff, SEEK_SET) != 0)
449             goto err;
450
451         for (itr = 0; itr < vec_size(pak->directories); itr++) {
452             fs_file_write(&(pak->directories[itr]), sizeof(pak_directory_t), 1, pak->handle);
453         }
454     }
455
456     vec_free     (pak->directories);
457     fs_file_close(pak->handle);
458     mem_d        (pak);
459
460     return true;
461
462 err:
463     vec_free     (pak->directories);
464     fs_file_close(pak->handle);
465     mem_d        (pak);
466
467     return false;
468 }
469
470 /*
471  * Fancy GCC-like LONG parsing allows things like --opt=param with
472  * assignment operator.  This is used for redirecting stdout/stderr
473  * console to specific files of your choice.
474  */
475 static bool parsecmd(const char *optname, int *argc_, char ***argv_, char **out, int ds, bool split) {
476     int  argc   = *argc_;
477     char **argv = *argv_;
478
479     size_t len = strlen(optname);
480
481     if (strncmp(argv[0]+ds, optname, len))
482         return false;
483
484     /* it's --optname, check how the parameter is supplied */
485     if (argv[0][ds+len] == '=') {
486         *out = argv[0]+ds+len+1;
487         return true;
488     }
489
490     if (!split || argc < ds) /* no parameter was provided, or only single-arg form accepted */
491         return false;
492
493     /* using --opt param */
494     *out = argv[1];
495     --*argc_;
496     ++*argv_;
497     return true;
498 }
499
500 int main(int argc, char **argv) {
501     bool          extract   = true;
502     char         *redirout  = (char*)stdout;
503     char         *redirerr  = (char*)stderr;
504     char         *file      = NULL;
505     char        **files     = NULL;
506     pak_file_t   *pak       = NULL;
507     size_t        iter      = 0;
508
509     con_init();
510
511     /*
512      * Command line option parsing commences now We only need to support
513      * a few things in the test suite.
514      */
515     while (argc > 1) {
516         ++argv;
517         --argc;
518
519         if (argv[0][0] == '-') {
520             if (parsecmd("redirout",  &argc, &argv, &redirout,  1, false))
521                 continue;
522             if (parsecmd("redirerr",  &argc, &argv, &redirerr,  1, false))
523                 continue;
524             if (parsecmd("file",      &argc, &argv, &file,      1, false))
525                 continue;
526
527             con_change(redirout, redirerr);
528
529             switch (argv[0][1]) {
530                 case 'e': extract = true;  continue;
531                 case 'c': extract = false; continue;
532             }
533
534             if (!strcmp(argv[0]+1, "debug")) {
535                 OPTS_OPTION_BOOL(OPTION_DEBUG) = true;
536                 continue;
537             }
538             if (!strcmp(argv[0]+1, "memchk")) {
539                 OPTS_OPTION_BOOL(OPTION_MEMCHK) = true;
540                 continue;
541             }
542             if (!strcmp(argv[0]+1, "nocolor")) {
543                 con_color(0);
544                 continue;
545             }
546         }
547
548         vec_push(files, argv[0]);
549     }
550     con_change(redirout, redirerr);
551
552
553     if (!file) {
554         con_err("-file must be specified for output/input PAK file\n");
555         vec_free(files);
556         return EXIT_FAILURE;
557     }
558
559     if (extract) {
560         if (!(pak = pak_open(file, "r"))) {
561             con_err("failed to open PAK file %s\n", file);
562             vec_free(files);
563             return EXIT_FAILURE;
564         }
565
566         if (!pak_extract_all(pak, "./")) {
567             con_err("failed to extract PAK %s (files may be missing)\n", file);
568             pak_close(pak);
569             vec_free(files);
570             return EXIT_FAILURE;
571         }
572
573         /* not possible */
574         pak_close(pak);
575         vec_free(files);
576         stat_info();
577
578         return EXIT_SUCCESS;
579     }
580
581     if (!(pak = pak_open(file, "w"))) {
582         con_err("failed to open PAK %s for writing\n", file);
583         vec_free(files);
584         return EXIT_FAILURE;
585     }
586
587     for (iter = 0; iter < vec_size(files); iter++) {
588         if (!(pak_insert_one(pak, files[iter]))) {
589             con_err("failed inserting %s for PAK %s\n", files[iter], file);
590             pak_close(pak);
591             vec_free(files);
592             return EXIT_FAILURE;
593         }
594     }
595
596     /* not possible */
597     pak_close(pak);
598     vec_free(files);
599
600     stat_info();
601     return EXIT_SUCCESS;
602 }