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