1 #ifdef RCSID
2 static char RCSid[] =
3 "$Header$";
4 #endif
5 
6 /*
7  *   Copyright (c) 1999, 2002 Michael J. Roberts.  All Rights Reserved.
8  *
9  *   Please see the accompanying license file, LICENSE.TXT, for information
10  *   on using and copying this software.
11  */
12 /*
13 Name
14   vmmain.cpp - T3 VM main entrypoint - execute an image file
15 Function
16   Main entrypoint for executing a T3 image file.  Loads an image file
17   and begins execution.  Returns when execution terminates.
18 Notes
19 
20 Modified
21   10/07/99 MJRoberts  - Creation
22 */
23 
24 #include <stdlib.h>
25 #include <string.h>
26 #include <stdio.h>
27 
28 #include "os.h"
29 #include "t3std.h"
30 #include "vmerr.h"
31 #include "vmfile.h"
32 #include "vmimage.h"
33 #include "vmrun.h"
34 #include "vmimgrb.h"
35 #include "vmmain.h"
36 #include "vmhost.h"
37 #include "vmhostsi.h"
38 #include "vminit.h"
39 #include "vmpredef.h"
40 #include "vmobj.h"
41 #include "vmvsn.h"
42 #include "charmap.h"
43 #include "vmsave.h"
44 #include "vmtype.h"
45 #include "vmrunsym.h"
46 
47 
48 /* ------------------------------------------------------------------------ */
49 /*
50  *   Execute an image file.  If an exception occurs, we'll display a
51  *   message on the console, and we'll return the error code; we'll return
52  *   zero on success.  If an error occurs, we'll fill in 'errbuf' with a
53  *   message describing the problem.
54  */
vm_run_image(CVmMainClientIfc * clientifc,const char * image_file_name,class CVmHostIfc * hostifc,const char * const * prog_argv,int prog_argc,const char * script_file,const char * log_file,const char * cmd_log_file,int load_from_exe,int show_banner,const char * charset,const char * saved_state,const char * res_dir)55 int vm_run_image(CVmMainClientIfc *clientifc,
56                  const char *image_file_name,
57                  class CVmHostIfc *hostifc,
58                  const char *const *prog_argv, int prog_argc,
59                  const char *script_file, const char *log_file,
60                  const char *cmd_log_file,
61                  int load_from_exe, int show_banner, const char *charset,
62                  const char *saved_state, const char *res_dir)
63 {
64     CVmFile *fp = 0;
65     CVmImageLoader *volatile loader = 0;
66     CVmImageFile *volatile imagefp = 0;
67     unsigned long image_file_base = 0;
68     int retval;
69     vm_globals *vmg__;
70 
71     /* presume we will return success */
72     retval = 0;
73 
74     /* create the file object */
75     fp = new CVmFile();
76 
77     /* initialize the VM */
78     vm_initialize(&vmg__, hostifc, clientifc, charset);
79 
80     /* tell the client system to initialize */
81     clientifc->client_init(VMGLOB_ADDR, script_file, log_file,
82                            cmd_log_file,
83                            show_banner ? T3VM_BANNER_STRING : 0);
84 
85     /* catch any errors that occur during loading and running */
86     err_try
87     {
88         if (load_from_exe)
89         {
90             osfildef *exe_fp;
91 
92             /* find the image within the executable */
93             exe_fp = os_exeseek(image_file_name, "TGAM");
94             if (exe_fp == 0)
95                 err_throw(VMERR_NO_IMAGE_IN_EXE);
96 
97             /*
98              *   set up to read from the executable at the location of the
99              *   embedded image file that we just found
100              */
101             image_file_base = osfpos(exe_fp);
102             fp->set_file(exe_fp, image_file_base);
103         }
104         else
105         {
106             /* reading from a normal file - open the file */
107             fp->open_read(image_file_name, OSFTT3IMG);
108         }
109 
110         /* create the loader */
111         imagefp = new CVmImageFileExt(fp);
112         loader = new CVmImageLoader(imagefp, image_file_name,
113                                     image_file_base);
114 
115         /* load the image */
116         loader->load(vmg0_);
117 
118         /* if we have a resource root path, tell the host interface */
119         if (res_dir != 0)
120             hostifc->set_res_dir(res_dir);
121 
122         /* let the client prepare for execution */
123         clientifc->pre_exec(VMGLOB_ADDR);
124 
125         /* run the program from the main entrypoint */
126         loader->run(vmg_ prog_argv, prog_argc, 0, saved_state);
127 
128         /* tell the client we're done with execution */
129         clientifc->post_exec(VMGLOB_ADDR);
130     }
131     err_catch(exc)
132     {
133         char errbuf[512];
134 
135         /* tell the client execution failed due to an error */
136         clientifc->post_exec_err(VMGLOB_ADDR);
137 
138         /* note the error code for returning to the caller */
139         retval = exc->get_error_code();
140 
141         /* get the message for the error */
142         CVmRun::get_exc_message(vmg_ exc, errbuf, sizeof(errbuf), TRUE);
143 
144         /* display the message */
145         clientifc->display_error(VMGLOB_ADDR, errbuf, FALSE);
146     }
147     err_end;
148 
149     /* unload the image */
150     if (loader != 0)
151         loader->unload(vmg0_);
152 
153     /* delete the loader and the image file object */
154     if (loader != 0)
155         delete loader;
156     if (imagefp != 0)
157         delete imagefp;
158 
159     /* notify the client */
160     clientifc->client_terminate(VMGLOB_ADDR);
161 
162     /* terminate the VM */
163     vm_terminate(vmg__, clientifc);
164 
165     /* delete the file */
166     if (fp != 0)
167         delete fp;
168 
169     /* return the status code */
170     return retval;
171 }
172 
173 /* ------------------------------------------------------------------------ */
174 /*
175  *   Read an option argument from an option that allows its arguments to be
176  *   either appended directly to the end of the option (as in "-otest") or
177  *   to follow as the subsequent vector item (as in "-o test").  optlen
178  *   gives the length of the option prefix itself, including the hyphen
179  *   prefix (so an option "-o" would have length 2).  We'll increment
180  *   *curarg if we consume the extra vector position.  Returns null if there
181  *   isn't an argument for the option.
182  */
get_opt_arg(int argc,char ** argv,int * curarg,int optlen)183 static char *get_opt_arg(int argc, char **argv, int *curarg, int optlen)
184 {
185     /*
186      *   if we have an argument (i.e., any non-empty text) appended directly
187      *   to the option vector item, we don't need to consume an extra vector
188      *   position
189      */
190     if (argv[*curarg][optlen] != '\0')
191     {
192         /* the argument is merely the remainder of this vector item */
193         return &argv[*curarg][optlen];
194     }
195 
196     /* advance to the next vector position */
197     ++(*curarg);
198 
199     /* if we don't have any vector items left, there's no argument */
200     if (*curarg >= argc)
201         return 0;
202 
203     /* this vector item is the next argument */
204     return argv[*curarg];
205 }
206 
207 /* ------------------------------------------------------------------------ */
208 /*
209  *   Main Entrypoint for command-line invocations.  For simplicity, a
210  *   normal C main() or equivalent entrypoint can invoke this routine
211  *   directly, using the usual argc/argv conventions.
212  *
213  *   Returns a status code suitable for use with exit(): OSEXSUCC if we
214  *   successfully loaded and ran an executable, OSEXFAIL on failure.  If
215  *   an error occurs, we'll fill in 'errbuf' with a message describing the
216  *   problem.
217  */
vm_run_image_main(CVmMainClientIfc * clientifc,const char * executable_name,int argc,char ** argv,int defext,int test_mode,CVmHostIfc * hostifc)218 int vm_run_image_main(CVmMainClientIfc *clientifc,
219                       const char *executable_name,
220                       int argc, char **argv, int defext, int test_mode,
221                       CVmHostIfc *hostifc)
222 {
223     int curarg;
224     char image_file_name[OSFNMAX];
225     int stat;
226     const char *script_file;
227     const char *log_file;
228     const char *cmd_log_file;
229     const char *res_dir;
230     int load_from_exe;
231     int show_banner;
232     int found_image;
233     int hide_usage;
234     int usage_err;
235     const char *charset;
236     char *saved_state;
237 
238     /* we haven't found an image file yet */
239     found_image = FALSE;
240 
241     /* presume we'll show usage on error */
242     hide_usage = FALSE;
243 
244     /* presume there will be no usage error */
245     usage_err = FALSE;
246 
247     /* presume we won't have any console input/output files */
248     script_file = 0;
249     log_file = 0;
250     cmd_log_file = 0;
251 
252     /* presume we'll use the default OS character set */
253     charset = 0;
254 
255     /* presume we won't show the banner */
256     show_banner = FALSE;
257 
258     /* presume we won't load from the .exe file */
259     load_from_exe = FALSE;
260 
261     /* presume we won't restore a saved state file */
262     saved_state = 0;
263 
264     /* presume we won't have a resource directory specified */
265     res_dir = 0;
266 
267     /* scan options */
268     for (curarg = 1 ; curarg < argc && argv[curarg][0] == '-' ; ++curarg)
269     {
270         /* check the argument */
271         switch(argv[curarg][1])
272         {
273         case 'b':
274             if (strcmp(argv[curarg], "-banner") == 0)
275             {
276                 /* make a note to show the banner */
277                 show_banner = TRUE;
278             }
279             else
280                 goto opt_error;
281             break;
282 
283         case 'c':
284             if (argv[curarg][2] == 's' && argv[curarg][3] == '\0')
285             {
286                 ++curarg;
287                 if (curarg < argc)
288                     charset = argv[curarg];
289                 else
290                     goto opt_error;
291             }
292             else
293                 goto opt_error;
294             break;
295 
296         case 'n':
297             if (strcmp(argv[curarg], "-nobanner") == 0)
298             {
299                 /* make a note not to show the banner */
300                 show_banner = FALSE;
301             }
302             else
303                 goto opt_error;
304             break;
305 
306         case 's':
307             /* file safety level - check the range */
308             if (argv[curarg][2] < '0' || argv[curarg][2] > '4'
309                 || argv[curarg][3] != '\0')
310             {
311                 /* invalid level */
312                 goto opt_error;
313             }
314             else
315             {
316                 /* set the level in the host application */
317                 hostifc->set_io_safety(argv[curarg][2] - '0');
318             }
319             break;
320 
321         case 'i':
322             /*
323              *   read from a script file - the next argument, or the
324              *   remainder of this argument, is the filename
325              */
326             script_file = get_opt_arg(argc, argv, &curarg, 2);
327             if (script_file == 0)
328                 goto opt_error;
329             break;
330 
331         case 'l':
332             /* log output to file */
333             log_file = get_opt_arg(argc, argv, &curarg, 2);
334             if (log_file == 0)
335                 goto opt_error;
336             break;
337 
338         case 'o':
339             /* log commands to file */
340             cmd_log_file = get_opt_arg(argc, argv, &curarg, 2);
341             if (cmd_log_file == 0)
342                 goto opt_error;
343             break;
344 
345         case 'p':
346             /* check what follows */
347             if (strcmp(argv[curarg], "-plain") == 0)
348             {
349                 /* tell the client to set plain ASCII mode */
350                 clientifc->set_plain_mode();
351                 break;
352             }
353             else
354                 goto opt_error;
355             break;
356 
357         case 'r':
358             /* get the name of the saved state file to restore */
359             saved_state = get_opt_arg(argc, argv, &curarg, 2);
360             if (saved_state == 0)
361                 goto opt_error;
362             break;
363 
364         case 'R':
365             /* note the resource root directory */
366             res_dir = get_opt_arg(argc, argv, &curarg, 2);
367             if (res_dir == 0)
368                 goto opt_error;
369             break;
370 
371         default:
372         opt_error:
373             /* discard remaining arguments */
374             curarg = argc;
375 
376             /* note the error */
377             usage_err = TRUE;
378             break;
379         }
380     }
381 
382     /*
383      *   If there was no usage error so far, but we don't have an image
384      *   filename argument, try to find the image file some other way.
385      */
386     if (usage_err)
387     {
388         /* there was a usage error - don't bother looking for an image file */
389     }
390     else if (curarg + 1 <= argc)
391     {
392         /* the last argument is the image file name */
393         strcpy(image_file_name, argv[curarg]);
394         found_image = TRUE;
395 
396         /*
397          *   If the given filename exists, use it as-is; otherwise, if
398          *   we're allowed to add an extension, try applying a default
399          *   extension of "t3" (formerly "t3x") to the given name.
400          */
401         if (defext && osfacc(image_file_name))
402         {
403             /* the given name doesn't exist - try a default extension */
404             os_defext(image_file_name, "t3");             /* formerly "t3x" */
405         }
406     }
407     else
408     {
409         osfildef *fp;
410 
411         /* look for an image file attached to the executable */
412         fp = os_exeseek(argv[0], "TGAM");
413         if (fp != 0)
414         {
415             /* close the file */
416             osfcls(fp);
417 
418             /* note that we want to load from the executable */
419             load_from_exe = TRUE;
420 
421             /* use the executable filename as the image file */
422             if (os_get_exe_filename(image_file_name,
423                                     sizeof(image_file_name), argv[0]))
424                 found_image = TRUE;
425         }
426 
427         /*
428          *   if we still haven't found an image file, try to get the image
429          *   file from the saved state file, if one was specified
430          */
431         if (!found_image && saved_state != 0)
432         {
433             osfildef *save_fp;
434 
435             /* open the saved state file */
436             save_fp = osfoprb(saved_state, OSFTT3SAV);
437             if (save_fp != 0)
438             {
439                 /* get the name of the image file */
440                 if (CVmSaveFile::restore_get_image(
441                     save_fp, image_file_name, sizeof(image_file_name)) == 0)
442                 {
443                     /* we successfully obtained the filename */
444                     found_image = TRUE;
445                 }
446 
447                 /* close the file */
448                 osfcls(save_fp);
449             }
450         }
451 
452         /*
453          *   if we haven't found the image, and the host system provides a
454          *   way of asking the user for a filename, try that
455          */
456         if (!found_image)
457         {
458             /* ask the host system for a game name */
459             switch (hostifc->get_image_name(image_file_name,
460                                             sizeof(image_file_name)))
461             {
462             case VMHOST_GIN_IGNORED:
463                 /* no effect - we have no new information */
464                 break;
465 
466             case VMHOST_GIN_CANCEL:
467                 /*
468                  *   the user cancelled the dialog - we don't have a
469                  *   filename, but we also don't want to show usage, since
470                  *   the user chose not to proceed
471                  */
472                 hide_usage = TRUE;
473                 break;
474 
475             case VMHOST_GIN_ERROR:
476                 /*
477                  *   an error occurred showing the dialog - there's not
478                  *   much we can do except show the usage message
479                  */
480                 break;
481 
482             case VMHOST_GIN_SUCCESS:
483                 /* that was successful - we have an image file now */
484                 found_image = TRUE;
485                 break;
486             }
487         }
488     }
489 
490     /*
491      *   if we don't have an image file name by this point, we can't
492      *   proceed - show the usage message and terminate
493      */
494     if (usage_err || !found_image)
495     {
496         char buf[OSFNMAX + 1024];
497 
498         /* show the usage message if allowed */
499         if (!hide_usage)
500         {
501             /* build the usage message */
502             sprintf(buf,
503                     "%s\n"
504                     "usage: %s [options] <image-file-name> [arguments]\n"
505                     "options:\n"
506                     "  -banner - show the version/copyright banner\n"
507                     "  -cs xxx - use character set 'xxx' for keyboard "
508                     "and display\n"
509                     "  -i file - read command line input from file\n"
510                     "  -l file - log all console input/output to file\n"
511                     "  -o file - log console input to file\n"
512                     "  -plain  - run in plain mode (no cursor positioning, "
513                     "colors, etc.)\n"
514                     "  -r file - restore saved state from file\n"
515                     "  -R dir  - set directory for external resources\n"
516                     "  -s#     - set I/O safety level (# in range 0 to 4 - 0 "
517                     "is the least\n"
518                     "            restrictive, 4 allows no file I/O at all)\n"
519                     "\n"
520                     "If provided, the optional arguments after the image "
521                     "file name are passed\n"
522                     "to the program's main entrypoint.\n",
523                     T3VM_BANNER_STRING, executable_name);
524 
525             /* display the message */
526             clientifc->display_error(0, buf, FALSE);
527         }
528 
529         /* return failure */
530         return OSEXFAIL;
531     }
532 
533     /*
534      *   if we're in test mode, replace the first argument to the program
535      *   with its root name, so that we don't include any path information
536      *   in the argument list
537      */
538     if (test_mode && curarg <= argc && argv[curarg] != 0)
539         argv[curarg] = os_get_root_name(argv[curarg]);
540 
541     /* run the program */
542     stat = vm_run_image(clientifc, image_file_name, hostifc,
543                         argv + curarg, argc - curarg,
544                         script_file, log_file, cmd_log_file,
545                         load_from_exe, show_banner, charset,
546                         saved_state, res_dir);
547 
548     /* return the status code */
549     return stat;
550 }
551 
552 /* ------------------------------------------------------------------------ */
553 /*
554  *   Copy a filename with a given size limit.  This works essentially like
555  *   strncpy(), but we guarantee that the result is null-terminated even if
556  *   it's truncated.
557  */
strcpy_limit(char * dst,const char * src,size_t limit)558 static void strcpy_limit(char *dst, const char *src, size_t limit)
559 {
560     size_t copy_len;
561 
562     /* get the length of the source string */
563     copy_len = strlen(src);
564 
565     /*
566      *   if it exceeds the available space, leaving space for the null
567      *   terminator, limit the copy to the available space
568      */
569     if (copy_len > limit - 1)
570         copy_len = limit - 1;
571 
572     /* copy as much as we can */
573     memcpy(dst, src, copy_len);
574 
575     /* null-terminate what we managed to copy */
576     dst[copy_len] = '\0';
577 }
578 
579 /* ------------------------------------------------------------------------ */
580 /*
581  *   Given a saved game file, try to identify the game file that created the
582  *   saved game.
583  */
vm_get_game_file_from_savefile(const char * savefile,char * fname,size_t fnamelen)584 static int vm_get_game_file_from_savefile(const char *savefile,
585                                           char *fname, size_t fnamelen)
586 {
587     osfildef *fp;
588     char buf[128];
589     int ret;
590     size_t len;
591 
592     /* open the saved game file */
593     fp = osfoprb(savefile, OSFTBIN);
594 
595     /* if that failed, there's no way to read the game file name */
596     if (fp == 0)
597         return FALSE;
598 
599     /* read the first few bytes */
600     if (osfrb(fp, buf, 16))
601     {
602         /*
603          *   we couldn't even read that much, so it must not really be a
604          *   saved game file
605          */
606         ret = FALSE;
607     }
608     else
609     {
610         /* check for a saved game signature we recognize */
611         if (memcmp(buf, "TADS2 save/g\012\015\032\000", 16) == 0)
612         {
613             /*
614              *   It's a TADS 2 saved game with embedded .GAM file
615              *   information.  The filename immediately follows the signature
616              *   (the 15 bytes we just matched), with a two-byte length
617              *   prefix.  Seek to the length prefix and read it.
618              */
619             osfseek(fp, 16, OSFSK_SET);
620             osfrb(fp, buf, 2);
621             len = osrp2(buf);
622 
623             /* limit the read length to our caller's available buffer */
624             if (len > fnamelen - 1)
625                 len = fnamelen - 1;
626 
627             /* read the filename and null-terminate it */
628             osfrb(fp, fname, len);
629             fname[len] = '\0';
630 
631             /* success */
632             ret = TRUE;
633         }
634         else if (memcmp(buf, "T3-state-v", 10) == 0)
635         {
636             /*
637              *   It's a T3 saved state file.  The image filename is always
638              *   embedded in this type of file, so seek back to the start of
639              *   the file and read the filename.
640              *
641              *   Note that restore_get_image() returns zero on success, so we
642              *   want to return true if and only if that routine returns
643              *   zero.
644              */
645             osfseek(fp, 0, OSFSK_SET);
646             ret = (CVmSaveFile::restore_get_image(fp, fname, fnamelen) == 0);
647         }
648         else
649         {
650             /*
651              *   it's not a signature we know, so it must not be a saved
652              *   state file (at least not one we can deal with)
653              */
654             ret = FALSE;
655         }
656     }
657 
658     /* we're done with the file now, so close it */
659     osfcls(fp);
660 
661     /* return the result */
662     return ret;
663 }
664 
665 
666 /* ------------------------------------------------------------------------ */
667 /*
668  *   Parse a command line to determine the name of the game file specified
669  *   by the arguments.  We'll return the index of the argv element that
670  *   specifies the name of the game, or a negative number if there is no
671  *   filename argument.
672  *
673  *   Note that our parsing will work for TADS 2 or TADS 3 interpreter
674  *   command lines, so this routine can be used to extract the filename from
675  *   an ambiguous command line in order to check the file for its type and
676  *   thereby resolve which interpreter to use.
677  */
vm_get_game_arg(int argc,const char * const * argv,char * buf,size_t buflen)678 int vm_get_game_arg(int argc, const char *const *argv,
679                     char *buf, size_t buflen)
680 {
681     int i;
682     const char *restore_file;
683 
684     /* presume we won't find a file to restore */
685     restore_file = 0;
686 
687     /*
688      *   Scan the arguments for the union of the TADS 2 and TADS 3 command
689      *   line options.  Start at the second element of the argument vector,
690      *   since the first element is the executable name.  Keep going until
691      *   we run out of options, each of which must start with a '-'.
692      *
693      *   Note that we don't care about the meanings of the options - we
694      *   simply want to skip past them.  This is more complicated than
695      *   merely looking for the first argument without a '-' prefix, because
696      *   some of the options allow arguments, and an option argument can
697      *   sometimes - depending on the argument - take the next position in
698      *   the argument vector after the option itself.  So, we must determine
699      *   the meaning of each option well enough that we can tell whether or
700      *   not the next argv element after the option is part of the option.
701      */
702     for (i = 1 ; i < argc && argv[i][0] == '-' ; ++i)
703     {
704         /*
705          *   check the first character after the hyphen to determine which
706          *   option we have
707          */
708         switch(argv[i][1])
709         {
710         case 'b':
711             /*
712              *   tads 3 "-banner" (no arguments, so just consume it and
713              *   continue)
714              */
715             break;
716 
717         case 'c':
718             /* tads 3 "-cs charset"; tads 2 "-ctab-" or "-ctab tab" */
719             if (strcmp(argv[i], "-ctab") == 0
720                 || strcmp(argv[i], "-cs") == 0)
721             {
722                 /* there's another argument giving the filename */
723                 ++i;
724             }
725             break;
726 
727         case 'd':
728             /* tads 2 "-double[+-]" (no arguments) */
729             break;
730 
731         case 'm':
732             /* tads 2 "-msSize", "-mhSize", "-mSize" */
733             switch(argv[i][2])
734             {
735             case 's':
736             case 'h':
737                 /*
738                  *   argument required - if nothing follows the second
739                  *   letter, consume the next vector item as the option
740                  *   argument
741                  */
742                 if (argv[i][3] == '\0')
743                     ++i;
744                 break;
745 
746             case '\0':
747                 /*
748                  *   argument required, but nothing follows the "-m" -
749                  *   consume the next vector item
750                  */
751                 ++i;
752                 break;
753 
754             default:
755                 /* argument required and present */
756                 break;
757             }
758             break;
759 
760         case 't':
761             /* tads 2 "-tfFile", "-tsSize", "-tp[+-]", "-t[+0]" */
762             switch(argv[i][2])
763             {
764             case 'f':
765             case 's':
766                 /*
767                  *   argument required - consume the next vector item if
768                  *   nothing is attached to this item
769                  */
770                 if (argv[i][3] == '\0')
771                     ++i;
772                 break;
773 
774             case 'p':
775                 /* no arguments */
776                 break;
777 
778             default:
779                 /* no arguments */
780                 break;
781             }
782             break;
783 
784         case 'u':
785             /*
786              *   tads 2 "-uSize" - argument required, so consume the next
787              *   vector item if necessary
788              */
789             if (argv[i][2] == '\0')
790                 ++i;
791             break;
792 
793         case 'n':
794             /* tads 3 "-nobanner" - no arguments */
795             break;
796 
797         case 's':
798             /*
799              *   tads 2/3 "-s#" (#=0,1,2,3,4); in tads 2, the # can be
800              *   separated by a space from the -s, so we'll allow it this
801              *   way in general
802              */
803             if (argv[i][2] == '\0')
804                 ++i;
805             break;
806 
807         case 'i':
808             /* tads 2/3 "-iFile" - consume an argument */
809             if (argv[i][2] == '\0')
810                 ++i;
811             break;
812 
813         case 'l':
814             /* tads 2/3 "-lFile" - consume an argument */
815             if (argv[i][2] == '\0')
816                 ++i;
817             break;
818 
819         case 'o':
820             /* tads 2/3 "-oFile" - consume an argument */
821             if (argv[i][2] == '\0')
822                 ++i;
823             break;
824 
825         case 'p':
826             /* tads 2/3 "-plain"; tads 2 "-p[+-]" - no arguments */
827             break;
828 
829         case 'R':
830             /* tads 3 '-Rdir' - consume an argument */
831             if (argv[i][2] == '\0')
832                 ++i;
833             break;
834 
835         case 'r':
836             /* tads 2/3 "-rFile" - consume an argument */
837             if (argv[i][2] != '\0')
838             {
839                 /* the file to be restored is appended to the "-r" */
840                 restore_file = argv[i] + 2;
841             }
842             else
843             {
844                 /* the file to be restored is the next argument */
845                 ++i;
846                 restore_file = argv[i];
847             }
848             break;
849         }
850     }
851 
852     /*
853      *   We have no more options, so the next argument is the game filename.
854      *   If we're out of argv elements, then no game filename was directly
855      *   specified, in which case we'll try looking for a file spec in the
856      *   restore file, if present.
857      */
858     if (i < argc)
859     {
860         /* there's a game file argument - copy it to the caller's buffer */
861         strcpy_limit(buf, argv[i], buflen);
862 
863         /* return success */
864         return TRUE;
865     }
866     else if (restore_file != 0)
867     {
868         /*
869          *   There's no game file argument, but there is a restore file
870          *   argument.  Try identifying the game file from the original game
871          *   file specification stored in the saved state file.
872          */
873         return vm_get_game_file_from_savefile(restore_file, buf, buflen);
874     }
875     else
876     {
877         /*
878          *   there's no game file or restore file argument, so they've given
879          *   us nothing to go on - there's no game file
880          */
881         return FALSE;
882     }
883 }
884 
885 /* ------------------------------------------------------------------------ */
886 /*
887  *   Given the name of a file, determine the engine (TADS 2 or TADS 3) for
888  *   which the file was compiled, based on the file's signature.  Returns
889  *   one of the VM_GGT_xxx codes.
890  */
vm_get_game_type_for_file(const char * filename)891 static int vm_get_game_type_for_file(const char *filename)
892 {
893     osfildef *fp;
894     char buf[16];
895     int ret;
896 
897     /* try opening the filename exactly as given */
898     fp = osfoprb(filename, OSFTBIN);
899 
900     /* if the file doesn't exist, tell the caller */
901     if (fp == 0)
902         return VM_GGT_NOT_FOUND;
903 
904     /* read the first few bytes of the file, where the signature resides */
905     if (osfrb(fp, buf, 16))
906     {
907         /*
908          *   error reading the file - any valid game file is going to be at
909          *   least long enough to hold the number of bytes we asked for, so
910          *   it must not be a valid file
911          */
912         ret = VM_GGT_INVALID;
913     }
914     else
915     {
916         /* check the signature we read against the known signatures */
917         if (memcmp(buf, "TADS2 bin\012\015\032", 12) == 0)
918             ret = VM_GGT_TADS2;
919         else if (memcmp(buf, "T3-image\015\012\032", 11) == 0)
920             ret = VM_GGT_TADS3;
921         else
922             ret = VM_GGT_INVALID;
923     }
924 
925     /* close the file */
926     osfcls(fp);
927 
928     /* return the version identifier */
929     return ret;
930 }
931 
932 /*
933  *   Given a game file argument, determine which engine (TADS 2 or TADS 3)
934  *   should be used to run the game.
935  */
vm_get_game_type(const char * filename,char * actual_fname,size_t actual_fname_len,const char * const * defexts,size_t defext_count)936 int vm_get_game_type(const char *filename,
937                      char *actual_fname, size_t actual_fname_len,
938                      const char *const *defexts, size_t defext_count)
939 {
940     int good_count;
941     int last_good_ver;
942     size_t i;
943 
944     /*
945      *   If the exact filename as given exists, determine the file type
946      *   directly from this file without trying any default extensions.
947      */
948     if (osfacc(filename) == 0)
949     {
950         /* the actual filename is exactly what we were given */
951         if (actual_fname_len != 0)
952         {
953             /* copy the filename, limiting it to the buffer length */
954             strncpy(actual_fname, filename, actual_fname_len);
955             actual_fname[actual_fname_len - 1] = '\0';
956         }
957 
958         /* return the type according to the file's signature */
959         return vm_get_game_type_for_file(filename);
960     }
961 
962     /* presume we won't find any good files using default extensions */
963     good_count = 0;
964 
965     /* try each default extension supplied */
966     for (i = 0 ; i < defext_count ; ++i)
967     {
968         int cur_ver;
969         char cur_fname[OSFNMAX];
970 
971         /*
972          *   build the default filename from the given filename and the
973          *   current default suffix
974          */
975         strcpy(cur_fname, filename);
976         os_defext(cur_fname, defexts[i]);
977 
978         /* get the version for this file */
979         cur_ver = vm_get_game_type_for_file(cur_fname);
980 
981         /* if it's a valid code, note it and remember it */
982         if (vm_ggt_is_valid(cur_ver))
983         {
984             /* it's a valid file - count it */
985             ++good_count;
986 
987             /* remember its version as the last good file's version */
988             last_good_ver = cur_ver;
989 
990             /* remember its name as the last good file's name */
991             if (actual_fname_len != 0)
992             {
993                 /* copy the filename, limiting it to the buffer length */
994                 strncpy(actual_fname, cur_fname, actual_fname_len);
995                 actual_fname[actual_fname_len - 1] = '\0';
996             }
997         }
998     }
999 
1000     /*
1001      *   If we had exactly one good match, return it.  We will already have
1002      *   filled in actual_fname with the last good filename, so all we need
1003      *   to do in this case is return the version ID for the last good file.
1004      */
1005     if (good_count == 1)
1006         return last_good_ver;
1007 
1008     /*
1009      *   If we didn't find any matches, tell the caller there is no match
1010      *   for the given filename.
1011      */
1012     if (good_count == 0)
1013         return VM_GGT_NOT_FOUND;
1014 
1015     /* we found more than one match, so the type is ambiguous */
1016     return VM_GGT_AMBIG;
1017 }
1018 
1019