]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/lib/util.c
To prevent breakage of existing scripts, reorder the commands.
[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_JOB_COPY:
365       str = _("Job Copy");
366       break;
367    case JT_CONSOLE:
368       str = _("Console");
369       break;
370    case JT_SYSTEM:
371       str = _("System or Console");
372       break;
373    case JT_SCAN:
374       str = _("Scan");
375       break;
376    default:
377       str = _("Unknown Type");
378       break;
379    }
380    return str;
381 }
382
383 /*
384  * Convert Job Level into a string
385  */
386 const char *job_level_to_str(int level)
387 {
388    const char *str;
389
390    switch (level) {
391    case L_BASE:
392       str = _("Base");
393    case L_FULL:
394       str = _("Full");
395       break;
396    case L_INCREMENTAL:
397       str = _("Incremental");
398       break;
399    case L_DIFFERENTIAL:
400       str = _("Differential");
401       break;
402    case L_SINCE:
403       str = _("Since");
404       break;
405    case L_VERIFY_CATALOG:
406       str = _("Verify Catalog");
407       break;
408    case L_VERIFY_INIT:
409       str = _("Verify Init Catalog");
410       break;
411    case L_VERIFY_VOLUME_TO_CATALOG:
412       str = _("Verify Volume to Catalog");
413       break;
414    case L_VERIFY_DISK_TO_CATALOG:
415       str = _("Verify Disk to Catalog");
416       break;
417    case L_VERIFY_DATA:
418       str = _("Verify Data");
419       break;
420    case L_VIRTUAL_FULL:
421       str = _("Virtual Full");
422       break;
423    case L_NONE:
424       str = " ";
425       break;
426    default:
427       str = _("Unknown Job Level");
428       break;
429    }
430    return str;
431 }
432
433 const char *volume_status_to_str(const char *status)
434 {
435    int pos;
436    const char *vs[] = {
437       NT_("Append"),    _("Append"),
438       NT_("Archive"),   _("Archive"),
439       NT_("Disabled"),  _("Disabled"),
440       NT_("Full"),      _("Full"),
441       NT_("Used"),      _("Used"),
442       NT_("Cleaning"),  _("Cleaning"),
443       NT_("Purged"),    _("Purged"),
444       NT_("Recycle"),   _("Recycle"),
445       NT_("Read-Only"), _("Read-Only"),
446       NT_("Error"),     _("Error"),
447       NULL,             NULL};
448
449    if (status) {
450      for (pos = 0 ; vs[pos] ; pos += 2) {
451        if ( !strcmp(vs[pos],status) ) {
452          return vs[pos+1];
453        }
454      }
455    }
456
457    return _("Invalid volume status");
458 }
459
460
461 /***********************************************************************
462  * Encode the mode bits into a 10 character string like LS does
463  ***********************************************************************/
464
465 char *encode_mode(mode_t mode, char *buf)
466 {
467   char *cp = buf;
468
469   *cp++ = S_ISDIR(mode) ? 'd' : S_ISBLK(mode)  ? 'b' : S_ISCHR(mode)  ? 'c' :
470           S_ISLNK(mode) ? 'l' : S_ISFIFO(mode) ? 'f' : S_ISSOCK(mode) ? 's' : '-';
471   *cp++ = mode & S_IRUSR ? 'r' : '-';
472   *cp++ = mode & S_IWUSR ? 'w' : '-';
473   *cp++ = (mode & S_ISUID
474                ? (mode & S_IXUSR ? 's' : 'S')
475                : (mode & S_IXUSR ? 'x' : '-'));
476   *cp++ = mode & S_IRGRP ? 'r' : '-';
477   *cp++ = mode & S_IWGRP ? 'w' : '-';
478   *cp++ = (mode & S_ISGID
479                ? (mode & S_IXGRP ? 's' : 'S')
480                : (mode & S_IXGRP ? 'x' : '-'));
481   *cp++ = mode & S_IROTH ? 'r' : '-';
482   *cp++ = mode & S_IWOTH ? 'w' : '-';
483   *cp++ = (mode & S_ISVTX
484                ? (mode & S_IXOTH ? 't' : 'T')
485                : (mode & S_IXOTH ? 'x' : '-'));
486   *cp = '\0';
487   return cp;
488 }
489
490 #if defined(HAVE_WIN32)
491 int do_shell_expansion(char *name, int name_len)
492 {
493    char *src = bstrdup(name);
494
495    ExpandEnvironmentStrings(src, name, name_len);
496
497    free(src);
498
499    return 1;
500 }
501 #else
502 int do_shell_expansion(char *name, int name_len)
503 {
504    static char meta[] = "~\\$[]*?`'<>\"";
505    bool found = false;
506    int len, i, stat;
507    POOLMEM *cmd;
508    BPIPE *bpipe;
509    char line[MAXSTRING];
510    const char *shellcmd;
511
512    /* Check if any meta characters are present */
513    len = strlen(meta);
514    for (i = 0; i < len; i++) {
515       if (strchr(name, meta[i])) {
516          found = true;
517          break;
518       }
519    }
520    if (found) {
521       cmd =  get_pool_memory(PM_FNAME);
522       /* look for shell */
523       if ((shellcmd = getenv("SHELL")) == NULL) {
524          shellcmd = "/bin/sh";
525       }
526       pm_strcpy(&cmd, shellcmd);
527       pm_strcat(&cmd, " -c \"echo ");
528       pm_strcat(&cmd, name);
529       pm_strcat(&cmd, "\"");
530       Dmsg1(400, "Send: %s\n", cmd);
531       if ((bpipe = open_bpipe(cmd, 0, "r"))) {
532          *line = 0;
533          fgets(line, sizeof(line), bpipe->rfd);
534          strip_trailing_junk(line);
535          stat = close_bpipe(bpipe);
536          Dmsg2(400, "stat=%d got: %s\n", stat, line);
537       } else {
538          stat = 1;                    /* error */
539       }
540       free_pool_memory(cmd);
541       if (stat == 0) {
542          bstrncpy(name, line, name_len);
543       }
544    }
545    return 1;
546 }
547 #endif
548
549
550 /*  MAKESESSIONKEY  --  Generate session key with optional start
551                         key.  If mode is TRUE, the key will be
552                         translated to a string, otherwise it is
553                         returned as 16 binary bytes.
554
555     from SpeakFreely by John Walker */
556
557 void make_session_key(char *key, char *seed, int mode)
558 {
559    int j, k;
560    struct MD5Context md5c;
561    unsigned char md5key[16], md5key1[16];
562    char s[1024];
563
564 #define ss sizeof(s)
565
566    s[0] = 0;
567    if (seed != NULL) {
568      bstrncat(s, seed, sizeof(s));
569    }
570
571    /* The following creates a seed for the session key generator
572      based on a collection of volatile and environment-specific
573      information unlikely to be vulnerable (as a whole) to an
574      exhaustive search attack.  If one of these items isn't
575      available on your machine, replace it with something
576      equivalent or, if you like, just delete it. */
577
578 #if defined(HAVE_WIN32)
579    {
580       LARGE_INTEGER     li;
581       DWORD             length;
582       FILETIME          ft;
583       char             *p;
584
585       p = s;
586       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)GetCurrentProcessId());
587       (void)getcwd(s + strlen(s), 256);
588       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)GetTickCount());
589       QueryPerformanceCounter(&li);
590       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)li.LowPart);
591       GetSystemTimeAsFileTime(&ft);
592       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)ft.dwLowDateTime);
593       bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)ft.dwHighDateTime);
594       length = 256;
595       GetComputerName(s + strlen(s), &length);
596       length = 256;
597       GetUserName(s + strlen(s), &length);
598    }
599 #else
600    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getpid());
601    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getppid());
602    (void)getcwd(s + strlen(s), 256);
603    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)clock());
604    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)time(NULL));
605 #if defined(Solaris)
606    sysinfo(SI_HW_SERIAL,s + strlen(s), 12);
607 #endif
608 #if defined(HAVE_GETHOSTID)
609    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t) gethostid());
610 #endif
611    gethostname(s + strlen(s), 256);
612    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getuid());
613    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)getgid());
614 #endif
615    MD5Init(&md5c);
616    MD5Update(&md5c, (uint8_t *)s, strlen(s));
617    MD5Final(md5key, &md5c);
618    bsnprintf(s + strlen(s), ss, "%lu", (uint32_t)((time(NULL) + 65121) ^ 0x375F));
619    MD5Init(&md5c);
620    MD5Update(&md5c, (uint8_t *)s, strlen(s));
621    MD5Final(md5key1, &md5c);
622 #define nextrand    (md5key[j] ^ md5key1[j])
623    if (mode) {
624      for (j = k = 0; j < 16; j++) {
625         unsigned char rb = nextrand;
626
627 #define Rad16(x) ((x) + 'A')
628         key[k++] = Rad16((rb >> 4) & 0xF);
629         key[k++] = Rad16(rb & 0xF);
630 #undef Rad16
631         if (j & 1) {
632            key[k++] = '-';
633         }
634      }
635      key[--k] = 0;
636    } else {
637      for (j = 0; j < 16; j++) {
638         key[j] = nextrand;
639      }
640    }
641 }
642 #undef nextrand
643
644 void encode_session_key(char *encode, char *session, char *key, int maxlen)
645 {
646    int i;
647    for (i=0; (i < maxlen-1) && session[i]; i++) {
648       if (session[i] == '-') {
649          encode[i] = '-';
650       } else {
651          encode[i] = ((session[i] - 'A' + key[i]) & 0xF) + 'A';
652       }
653    }
654    encode[i] = 0;
655    Dmsg3(000, "Session=%s key=%s encode=%s\n", session, key, encode);
656 }
657
658 void decode_session_key(char *decode, char *session, char *key, int maxlen)
659 {
660    int i, x;
661
662    for (i=0; (i < maxlen-1) && session[i]; i++) {
663       if (session[i] == '-') {
664          decode[i] = '-';
665       } else {
666          x = (session[i] - 'A' - key[i]) & 0xF;
667          if (x < 0) {
668             x += 16;
669          }
670          decode[i] = x + 'A';
671       }
672    }
673    decode[i] = 0;
674    Dmsg3(000, "Session=%s key=%s decode=%s\n", session, key, decode);
675 }
676
677
678
679 /*
680  * Edit job codes into main command line
681  *  %% = %
682  *  %c = Client's name
683  *  %d = Director's name
684  *  %e = Job Exit code
685  *  %i = JobId
686  *  %j = Unique Job id
687  *  %l = job level
688  *  %n = Unadorned Job name
689  *  %s = Since time
690  *  %t = Job type (Backup, ...)
691  *  %r = Recipients
692  *  %v = Volume name
693  *
694  *  omsg = edited output message
695  *  imsg = input string containing edit codes (%x)
696  *  to = recepients list
697  *
698  */
699 POOLMEM *edit_job_codes(JCR *jcr, char *omsg, char *imsg, const char *to, job_code_callback_t callback)
700 {
701    char *p, *q;
702    const char *str;
703    char add[20];
704    char name[MAX_NAME_LENGTH];
705    int i;
706
707    *omsg = 0;
708    Dmsg1(200, "edit_job_codes: %s\n", imsg);
709    for (p=imsg; *p; p++) {
710       if (*p == '%') {
711          switch (*++p) {
712          case '%':
713             str = "%";
714             break;
715          case 'c':
716             if (jcr) {
717                str = jcr->client_name;
718             } else {
719                str = _("*none*");
720             }
721             break;
722          case 'd':
723             str = my_name;            /* Director's name */
724             break;
725          case 'e':
726             if (jcr) {
727                str = job_status_to_str(jcr->JobStatus);
728             } else {
729                str = _("*none*");
730             }
731             break;
732          case 'i':
733             if (jcr) {
734                bsnprintf(add, sizeof(add), "%d", jcr->JobId);
735                str = add;
736             } else {
737                str = _("*none*");
738             }
739             break;
740          case 'j':                    /* Job name */
741             if (jcr) {
742                str = jcr->Job;
743             } else {
744                str = _("*none*");
745             }
746             break;
747          case 'l':
748             if (jcr) {
749                str = job_level_to_str(jcr->get_JobLevel());
750             } else {
751                str = _("*none*");
752             }
753             break;
754          case 'n':
755              if (jcr) {
756                 bstrncpy(name, jcr->Job, sizeof(name));
757                 /* There are three periods after the Job name */
758                 for (i=0; i<3; i++) {
759                    if ((q=strrchr(name, '.')) != NULL) {
760                        *q = 0;
761                    }
762                 }
763                 str = name;
764              } else {
765                 str = _("*none*");
766              }
767              break;
768          case 'r':
769             str = to;
770             break;
771          case 's':                    /* since time */
772             if (jcr && jcr->stime) {
773                str = jcr->stime;
774             } else {
775                str = _("*none*");
776             }
777             break;
778          case 't':
779             if (jcr) {
780                str = job_type_to_str(jcr->get_JobType());
781             } else {
782                str = _("*none*");
783             }
784             break;
785          case 'v':
786             if (jcr) {
787                if (jcr->VolumeName && jcr->VolumeName[0]) {
788                   str = jcr->VolumeName;
789                } else {
790                   str = "";
791                }
792             } else {
793                str = _("*none*");
794             }
795             break;
796          default:
797             str = NULL;
798             if (callback != NULL) {
799                 str = callback(jcr, p);
800             }
801
802             if (!str) {
803                 add[0] = '%';
804                 add[1] = *p;
805                 add[2] = 0;
806                 str = add;
807             }
808             break;
809          }
810       } else {
811          add[0] = *p;
812          add[1] = 0;
813          str = add;
814       }
815       Dmsg1(1200, "add_str %s\n", str);
816       pm_strcat(&omsg, str);
817       Dmsg1(1200, "omsg=%s\n", omsg);
818    }
819    return omsg;
820 }
821
822 void set_working_directory(char *wd)
823 {
824    struct stat stat_buf;
825
826    if (wd == NULL) {
827       Emsg0(M_ERROR_TERM, 0, _("Working directory not defined. Cannot continue.\n"));
828    }
829    if (stat(wd, &stat_buf) != 0) {
830       Emsg1(M_ERROR_TERM, 0, _("Working Directory: \"%s\" not found. Cannot continue.\n"),
831          wd);
832    }
833    if (!S_ISDIR(stat_buf.st_mode)) {
834       Emsg1(M_ERROR_TERM, 0, _("Working Directory: \"%s\" is not a directory. Cannot continue.\n"),
835          wd);
836    }
837    working_directory = wd;            /* set global */
838 }
839
840 const char *last_path_separator(const char *str)
841 {
842    if (*str != '\0') {
843       for (const char *p = &str[strlen(str) - 1]; p >= str; p--) {
844          if (IsPathSeparator(*p)) {
845             return p;
846          }
847       }
848    }
849    return NULL;
850 }