]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/lib/util.c
1d93d1b542be1beae8ddafe56de75bff6cfd6fbf
[bacula/bacula] / bacula / src / lib / util.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2000-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *   util.c  miscellaneous utility subroutines for Bacula
30  *
31  *    Kern Sibbald, MM
32  *
33  *   Version $Id$
34  */
35
36 #include "bacula.h"
37 #include "jcr.h"
38 #include "findlib/find.h"
39
40 /*
41  * Various Bacula Utility subroutines
42  *
43  */
44
45 /* Return true of buffer has all zero bytes */
46 bool is_buf_zero(char *buf, int len)
47 {
48    uint64_t *ip;
49    char *p;
50    int i, len64, done, rem;
51
52    if (buf[0] != 0) {
53       return false;
54    }
55    ip = (uint64_t *)buf;
56    /* Optimize by checking uint64_t for zero */
57    len64 = len / sizeof(uint64_t);
58    for (i=0; i < len64; i++) {
59       if (ip[i] != 0) {
60          return false;
61       }
62    }
63    done = len64 * sizeof(uint64_t);  /* bytes already checked */
64    p = buf + done;
65    rem = len - done;
66    for (i = 0; i < rem; i++) {
67       if (p[i] != 0) {
68          return false;
69       }
70    }
71    return true;
72 }
73
74
75 /* Convert a string in place to lower case */
76 void lcase(char *str)
77 {
78    while (*str) {
79       if (B_ISUPPER(*str)) {
80          *str = tolower((int)(*str));
81        }
82        str++;
83    }
84 }
85
86 /* Convert spaces to non-space character.
87  * This makes scanf of fields containing spaces easier.
88  */
89 void
90 bash_spaces(char *str)
91 {
92    while (*str) {
93       if (*str == ' ')
94          *str = 0x1;
95       str++;
96    }
97 }
98
99 /* Convert spaces to non-space character.
100  * This makes scanf of fields containing spaces easier.
101  */
102 void
103 bash_spaces(POOL_MEM &pm)
104 {
105    char *str = pm.c_str();
106    while (*str) {
107       if (*str == ' ')
108          *str = 0x1;
109       str++;
110    }
111 }
112
113
114 /* Convert non-space characters (0x1) back into spaces */
115 void
116 unbash_spaces(char *str)
117 {
118    while (*str) {
119      if (*str == 0x1)
120         *str = ' ';
121      str++;
122    }
123 }
124
125 /* Convert non-space characters (0x1) back into spaces */
126 void
127 unbash_spaces(POOL_MEM &pm)
128 {
129    char *str = pm.c_str();
130    while (*str) {
131      if (*str == 0x1)
132         *str = ' ';
133      str++;
134    }
135 }
136
137 char *encode_time(utime_t utime, char *buf)
138 {
139    struct tm tm;
140    int n = 0;
141    time_t time = utime;
142
143 #if defined(HAVE_WIN32)
144    /*
145     * Avoid a seg fault in Microsoft's CRT localtime_r(),
146     *  which incorrectly references a NULL returned from gmtime() if
147     *  time is negative before or after the timezone adjustment.
148     */
149    struct tm *gtm;
150
151    if ((gtm = gmtime(&time)) == NULL) {
152       return buf;
153    }
154
155    if (gtm->tm_year == 1970 && gtm->tm_mon == 1 && gtm->tm_mday < 3) {
156       return buf;
157    }
158 #endif
159
160    if (localtime_r(&time, &tm)) {
161       n = sprintf(buf, "%04d-%02d-%02d %02d:%02d:%02d",
162                    tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
163                    tm.tm_hour, tm.tm_min, tm.tm_sec);
164    }
165    return buf+n;
166 }
167
168
169
170 /*
171  * Convert a JobStatus code into a human readable form
172  */
173 void jobstatus_to_ascii(int JobStatus, char *msg, int maxlen)
174 {
175    const char *jobstat;
176    char buf[100];
177
178    switch (JobStatus) {
179    case JS_Created:
180       jobstat = _("Created");
181       break;
182    case JS_Running:
183       jobstat = _("Running");
184       break;
185    case JS_Blocked:
186       jobstat = _("Blocked");
187       break;
188    case JS_Terminated:
189       jobstat = _("OK");
190       break;
191    case JS_FatalError:
192    case JS_ErrorTerminated:
193       jobstat = _("Error");
194       break;
195    case JS_Error:
196       jobstat = _("Non-fatal error");
197       break;
198    case JS_Canceled:
199       jobstat = _("Canceled");
200       break;
201    case JS_Differences:
202       jobstat = _("Verify differences");
203       break;
204    case JS_WaitFD:
205       jobstat = _("Waiting on FD");
206       break;
207    case JS_WaitSD:
208       jobstat = _("Wait on SD");
209       break;
210    case JS_WaitMedia:
211       jobstat = _("Wait for new Volume");
212       break;
213    case JS_WaitMount:
214       jobstat = _("Waiting for mount");
215       break;
216    case JS_WaitStoreRes:
217       jobstat = _("Waiting for Storage resource");
218       break;
219    case JS_WaitJobRes:
220       jobstat = _("Waiting for Job resource");
221       break;
222    case JS_WaitClientRes:
223       jobstat = _("Waiting for Client resource");
224       break;
225    case JS_WaitMaxJobs:
226       jobstat = _("Waiting on Max Jobs");
227       break;
228    case JS_WaitStartTime:
229       jobstat = _("Waiting for Start Time");
230       break;
231    case JS_WaitPriority:
232       jobstat = _("Waiting on Priority");
233       break;
234    case JS_DataCommitting:
235       jobstat = _("SD committing Data");
236       break;
237    case JS_DataDespooling:
238       jobstat = _("SD despooling Data");
239       break;
240    case JS_AttrDespooling:
241       jobstat = _("SD despooling Attributes");
242       break;
243    case JS_AttrInserting:
244       jobstat = _("Dir inserting Attributes");
245       break;
246
247    default:
248       if (JobStatus == 0) {
249          buf[0] = 0;
250       } else {
251          bsnprintf(buf, sizeof(buf), _("Unknown Job termination status=%d"), JobStatus);
252       }
253       jobstat = buf;
254       break;
255    }
256    bstrncpy(msg, jobstat, maxlen);
257 }
258
259 /*
260  * Convert a JobStatus code into a human readable form - gui version
261  */
262 void jobstatus_to_ascii_gui(int JobStatus, char *msg, int maxlen)
263 {
264    const char *cnv = NULL;
265    switch (JobStatus) {
266    case JS_Terminated:
267       cnv = _("Completed successfully");
268       break;
269    case JS_ErrorTerminated:
270       cnv = _("Terminated with errors");
271       break;
272    case JS_FatalError:
273       cnv = _("Fatal error");
274       break;
275    case JS_Created:
276       cnv = _("Created, not yet running");
277       break;
278    case JS_Canceled:
279       cnv = _("Canceled by user");
280       break;
281    case JS_Differences:
282       cnv = _("Verify found differences");
283       break;
284    case JS_WaitFD:
285       cnv = _("Waiting for File daemon");
286       break;
287    case JS_WaitSD:
288       cnv = _("Waiting for Storage daemon");
289       break;
290    case JS_WaitPriority:
291       cnv = _("Waiting for higher priority jobs");
292       break;
293    case JS_AttrInserting:
294       cnv = _("Batch inserting file records");
295       break;
296    };
297
298    if (cnv) {
299       bstrncpy(msg, cnv, maxlen);
300    } else {
301      jobstatus_to_ascii( JobStatus, msg, maxlen);
302    }
303 }
304
305
306 /*
307  * Convert Job Termination Status into a string
308  */
309 const char *job_status_to_str(int stat)
310 {
311    const char *str;
312
313    switch (stat) {
314    case JS_Terminated:
315       str = _("OK");
316       break;
317    case JS_ErrorTerminated:
318    case JS_Error:
319       str = _("Error");
320       break;
321    case JS_FatalError:
322       str = _("Fatal Error");
323       break;
324    case JS_Canceled:
325       str = _("Canceled");
326       break;
327    case JS_Differences:
328       str = _("Differences");
329       break;
330    default:
331       str = _("Unknown term code");
332       break;
333    }
334    return str;
335 }
336
337
338 /*
339  * Convert Job Type into a string
340  */
341 const char *job_type_to_str(int type)
342 {
343    const char *str;
344
345    switch (type) {
346    case JT_BACKUP:
347       str = _("Backup");
348       break;
349    case JT_VERIFY:
350       str = _("Verify");
351       break;
352    case JT_RESTORE:
353       str = _("Restore");
354       break;
355    case JT_ADMIN:
356       str = _("Admin");
357       break;
358    case JT_MIGRATE:
359       str = _("Migrate");
360       break;
361    case JT_COPY:
362       str = _("Copy");
363       break;
364    case JT_CONSOLE:
365       str = _("Console");
366       break;
367    case JT_SYSTEM:
368       str = _("System or Console");
369       break;
370    case JT_SCAN:
371       str = _("Scan");
372       break;
373    default:
374       str = _("Unknown Type");
375       break;
376    }
377    return str;
378 }
379
380 /*
381  * Convert Job Level into a string
382  */
383 const char *job_level_to_str(int level)
384 {
385    const char *str;
386
387    switch (level) {
388    case L_BASE:
389       str = _("Base");
390    case L_FULL:
391       str = _("Full");
392       break;
393    case L_INCREMENTAL:
394       str = _("Incremental");
395       break;
396    case L_DIFFERENTIAL:
397       str = _("Differential");
398       break;
399    case L_SINCE:
400       str = _("Since");
401       break;
402    case L_VERIFY_CATALOG:
403       str = _("Verify Catalog");
404       break;
405    case L_VERIFY_INIT:
406       str = _("Verify Init Catalog");
407       break;
408    case L_VERIFY_VOLUME_TO_CATALOG:
409       str = _("Verify Volume to Catalog");
410       break;
411    case L_VERIFY_DISK_TO_CATALOG:
412       str = _("Verify Disk to Catalog");
413       break;
414    case L_VERIFY_DATA:
415       str = _("Verify Data");
416       break;
417    case L_VIRTUAL_FULL:
418       str = _("Virtual Full");
419       break;
420    case L_NONE:
421       str = " ";
422       break;
423    default:
424       str = _("Unknown Job Level");
425       break;
426    }
427    return str;
428 }
429
430 const char *volume_status_to_str(const char *status)
431 {
432    int pos;
433    const char *vs[] = {
434       NT_("Append"),    _("Append"),
435       NT_("Archive"),   _("Archive"),
436       NT_("Disabled"),  _("Disabled"),
437       NT_("Full"),      _("Full"),
438       NT_("Used"),      _("Used"),
439       NT_("Cleaning"),  _("Cleaning"),
440       NT_("Purged"),    _("Purged"),
441       NT_("Recycle"),   _("Recycle"),
442       NT_("Read-Only"), _("Read-Only"),
443       NT_("Error"),     _("Error"),
444       NULL,             NULL};
445
446    if (status) {
447      for (pos = 0 ; vs[pos] ; pos += 2) {
448        if ( !strcmp(vs[pos],status) ) {
449          return vs[pos+1];
450        }
451      }
452    }
453
454    return _("Invalid volume status");
455 }
456
457
458 /***********************************************************************
459  * Encode the mode bits into a 10 character string like LS does
460  ***********************************************************************/
461
462 char *encode_mode(mode_t mode, char *buf)
463 {
464   char *cp = buf;
465
466   *cp++ = S_ISDIR(mode) ? 'd' : S_ISBLK(mode)  ? 'b' : S_ISCHR(mode)  ? 'c' :
467           S_ISLNK(mode) ? 'l' : S_ISFIFO(mode) ? 'f' : S_ISSOCK(mode) ? 's' : '-';
468   *cp++ = mode & S_IRUSR ? 'r' : '-';
469   *cp++ = mode & S_IWUSR ? 'w' : '-';
470   *cp++ = (mode & S_ISUID
471                ? (mode & S_IXUSR ? 's' : 'S')
472                : (mode & S_IXUSR ? 'x' : '-'));
473   *cp++ = mode & S_IRGRP ? 'r' : '-';
474   *cp++ = mode & S_IWGRP ? 'w' : '-';
475   *cp++ = (mode & S_ISGID
476                ? (mode & S_IXGRP ? 's' : 'S')
477                : (mode & S_IXGRP ? 'x' : '-'));
478   *cp++ = mode & S_IROTH ? 'r' : '-';
479   *cp++ = mode & S_IWOTH ? 'w' : '-';
480   *cp++ = (mode & S_ISVTX
481                ? (mode & S_IXOTH ? 't' : 'T')
482                : (mode & S_IXOTH ? 'x' : '-'));
483   *cp = '\0';
484   return cp;
485 }
486
487 #if defined(HAVE_WIN32)
488 int do_shell_expansion(char *name, int name_len)
489 {
490    char *src = bstrdup(name);
491
492    ExpandEnvironmentStrings(src, name, name_len);
493
494    free(src);
495
496    return 1;
497 }
498 #else
499 int do_shell_expansion(char *name, int name_len)
500 {
501    static char meta[] = "~\\$[]*?`'<>\"";
502    bool found = false;
503    int len, i, stat;
504    POOLMEM *cmd;
505    BPIPE *bpipe;
506    char line[MAXSTRING];
507    const char *shellcmd;
508
509    /* Check if any meta characters are present */
510    len = strlen(meta);
511    for (i = 0; i < len; i++) {
512       if (strchr(name, meta[i])) {
513          found = true;
514          break;
515       }
516    }
517    if (found) {
518       cmd =  get_pool_memory(PM_FNAME);
519       /* look for shell */
520       if ((shellcmd = getenv("SHELL")) == NULL) {
521          shellcmd = "/bin/sh";
522       }
523       pm_strcpy(&cmd, shellcmd);
524       pm_strcat(&cmd, " -c \"echo ");
525       pm_strcat(&cmd, name);
526       pm_strcat(&cmd, "\"");
527       Dmsg1(400, "Send: %s\n", cmd);
528       if ((bpipe = open_bpipe(cmd, 0, "r"))) {
529          *line = 0;
530          fgets(line, sizeof(line), bpipe->rfd);
531          strip_trailing_junk(line);
532          stat = close_bpipe(bpipe);
533          Dmsg2(400, "stat=%d got: %s\n", stat, line);
534       } else {
535          stat = 1;                    /* error */
536       }
537       free_pool_memory(cmd);
538       if (stat == 0) {
539          bstrncpy(name, line, name_len);
540       }
541    }
542    return 1;
543 }
544 #endif
545
546
547 /*  MAKESESSIONKEY  --  Generate session key with optional start
548                         key.  If mode is TRUE, the key will be
549                         translated to a string, otherwise it is
550                         returned as 16 binary bytes.
551
552     from SpeakFreely by John Walker */
553
554 void make_session_key(char *key, char *seed, int mode)
555 {
556    int j, k;
557    struct MD5Context md5c;
558    unsigned char md5key[16], md5key1[16];
559    char s[1024];
560
561 #define ss sizeof(s)
562
563    s[0] = 0;
564    if (seed != NULL) {
565      bstrncat(s, seed, sizeof(s));
566    }
567
568    /* The following creates a seed for the session key generator
569      based on a collection of volatile and environment-specific
570      information unlikely to be vulnerable (as a whole) to an
571      exhaustive search attack.  If one of these items isn't
572      available on your machine, replace it with something
573      equivalent or, if you like, just delete it. */
574
575 #if defined(HAVE_WIN32)
576    {
577       LARGE_INTEGER     li;
578       DWORD             length;
579       FILETIME          ft;
580       char             *p;
581
582       p = s;
583       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)GetCurrentProcessId());
584       (void)getcwd(s + strlen(s), 256);
585       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)GetTickCount());
586       QueryPerformanceCounter(&li);
587       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)li.LowPart);
588       GetSystemTimeAsFileTime(&ft);
589       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)ft.dwLowDateTime);
590       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)ft.dwHighDateTime);
591       length = 256;
592       GetComputerName(s + strlen(s), &length);
593       length = 256;
594       GetUserName(s + strlen(s), &length);
595    }
596 #else
597    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getpid());
598    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getppid());
599    (void)getcwd(s + strlen(s), 256);
600    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)clock());
601    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)time(NULL));
602 #if defined(Solaris)
603    sysinfo(SI_HW_SERIAL,s + strlen(s), 12);
604 #endif
605 #if defined(HAVE_GETHOSTID)
606    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t) gethostid());
607 #endif
608    gethostname(s + strlen(s), 256);
609    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getuid());
610    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getgid());
611 #endif
612    MD5Init(&md5c);
613    MD5Update(&md5c, (uint8_t *)s, strlen(s));
614    MD5Final(md5key, &md5c);
615    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)((time(NULL) + 65121) ^ 0x375F));
616    MD5Init(&md5c);
617    MD5Update(&md5c, (uint8_t *)s, strlen(s));
618    MD5Final(md5key1, &md5c);
619 #define nextrand    (md5key[j] ^ md5key1[j])
620    if (mode) {
621      for (j = k = 0; j < 16; j++) {
622         unsigned char rb = nextrand;
623
624 #define Rad16(x) ((x) + 'A')
625         key[k++] = Rad16((rb >> 4) & 0xF);
626         key[k++] = Rad16(rb & 0xF);
627 #undef Rad16
628         if (j & 1) {
629            key[k++] = '-';
630         }
631      }
632      key[--k] = 0;
633    } else {
634      for (j = 0; j < 16; j++) {
635         key[j] = nextrand;
636      }
637    }
638 }
639 #undef nextrand
640
641 void encode_session_key(char *encode, char *session, char *key, int maxlen)
642 {
643    int i;
644    for (i=0; (i < maxlen-1) && session[i]; i++) {
645       if (session[i] == '-') {
646          encode[i] = '-';
647       } else {
648          encode[i] = ((session[i] - 'A' + key[i]) & 0xF) + 'A';
649       }
650    }
651    encode[i] = 0;
652    Dmsg3(000, "Session=%s key=%s encode=%s\n", session, key, encode);
653 }
654
655 void decode_session_key(char *decode, char *session, char *key, int maxlen)
656 {
657    int i, x;
658
659    for (i=0; (i < maxlen-1) && session[i]; i++) {
660       if (session[i] == '-') {
661          decode[i] = '-';
662       } else {
663          x = (session[i] - 'A' - key[i]) & 0xF;
664          if (x < 0) {
665             x += 16;
666          }
667          decode[i] = x + 'A';
668       }
669    }
670    decode[i] = 0;
671    Dmsg3(000, "Session=%s key=%s decode=%s\n", session, key, decode);
672 }
673
674
675
676 /*
677  * Edit job codes into main command line
678  *  %% = %
679  *  %c = Client's name
680  *  %d = Director's name
681  *  %e = Job Exit code
682  *  %i = JobId
683  *  %j = Unique Job id
684  *  %l = job level
685  *  %n = Unadorned Job name
686  *  %s = Since time
687  *  %t = Job type (Backup, ...)
688  *  %r = Recipients
689  *  %v = Volume name
690  *
691  *  omsg = edited output message
692  *  imsg = input string containing edit codes (%x)
693  *  to = recepients list
694  *
695  */
696 POOLMEM *edit_job_codes(JCR *jcr, char *omsg, char *imsg, const char *to, job_code_callback_t callback)
697 {
698    char *p, *q;
699    const char *str;
700    char add[20];
701    char name[MAX_NAME_LENGTH];
702    int i;
703
704    *omsg = 0;
705    Dmsg1(200, "edit_job_codes: %s\n", imsg);
706    for (p=imsg; *p; p++) {
707       if (*p == '%') {
708          switch (*++p) {
709          case '%':
710             str = "%";
711             break;
712          case 'c':
713             if (jcr) {
714                str = jcr->client_name;
715             } else {
716                str = _("*none*");
717             }
718             break;
719          case 'd':
720             str = my_name;            /* Director's name */
721             break;
722          case 'e':
723             if (jcr) {
724                str = job_status_to_str(jcr->JobStatus);
725             } else {
726                str = _("*none*");
727             }
728             break;
729          case 'i':
730             if (jcr) {
731                bsnprintf(add, sizeof(add), "%d", jcr->JobId);
732                str = add;
733             } else {
734                str = _("*none*");
735             }
736             break;
737          case 'j':                    /* Job name */
738             if (jcr) {
739                str = jcr->Job;
740             } else {
741                str = _("*none*");
742             }
743             break;
744          case 'l':
745             if (jcr) {
746                str = job_level_to_str(jcr->get_JobLevel());
747             } else {
748                str = _("*none*");
749             }
750             break;
751          case 'n':
752              if (jcr) {
753                 bstrncpy(name, jcr->Job, sizeof(name));
754                 /* There are three periods after the Job name */
755                 for (i=0; i<3; i++) {
756                    if ((q=strrchr(name, '.')) != NULL) {
757                        *q = 0;
758                    }
759                 }
760                 str = name;
761              } else {
762                 str = _("*none*");
763              }
764              break;
765          case 'r':
766             str = to;
767             break;
768          case 's':                    /* since time */
769             if (jcr && jcr->stime) {
770                str = jcr->stime;
771             } else {
772                str = _("*none*");
773             }
774             break;
775          case 't':
776             if (jcr) {
777                str = job_type_to_str(jcr->get_JobType());
778             } else {
779                str = _("*none*");
780             }
781             break;
782          case 'v':
783             if (jcr) {
784                if (jcr->VolumeName && jcr->VolumeName[0]) {
785                   str = jcr->VolumeName;
786                } else {
787                   str = "";
788                }
789             } else {
790                str = _("*none*");
791             }
792             break;
793          default:
794             str = NULL;
795             if (callback != NULL) {
796                 str = callback(jcr, p);
797             }
798
799             if (!str) {
800                 add[0] = '%';
801                 add[1] = *p;
802                 add[2] = 0;
803                 str = add;
804             }
805             break;
806          }
807       } else {
808          add[0] = *p;
809          add[1] = 0;
810          str = add;
811       }
812       Dmsg1(1200, "add_str %s\n", str);
813       pm_strcat(&omsg, str);
814       Dmsg1(1200, "omsg=%s\n", omsg);
815    }
816    return omsg;
817 }
818
819 void set_working_directory(char *wd)
820 {
821    struct stat stat_buf;
822
823    if (wd == NULL) {
824       Emsg0(M_ERROR_TERM, 0, _("Working directory not defined. Cannot continue.\n"));
825    }
826    if (stat(wd, &stat_buf) != 0) {
827       Emsg1(M_ERROR_TERM, 0, _("Working Directory: \"%s\" not found. Cannot continue.\n"),
828          wd);
829    }
830    if (!S_ISDIR(stat_buf.st_mode)) {
831       Emsg1(M_ERROR_TERM, 0, _("Working Directory: \"%s\" is not a directory. Cannot continue.\n"),
832          wd);
833    }
834    working_directory = wd;            /* set global */
835 }
836
837 const char *last_path_separator(const char *str)
838 {
839    if (*str != '\0') {
840       for (const char *p = &str[strlen(str) - 1]; p >= str; p--) {
841          if (IsPathSeparator(*p)) {
842             return p;
843          }
844       }
845    }
846    return NULL;
847 }