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