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