1 /*
2 Bacula(R) - The Network Backup Solution
3
4 Copyright (C) 2000-2020 Kern Sibbald
5
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many others, a complete list can be found in the file AUTHORS.
8
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
13
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
16
17 Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19
20 /* You can include this file to your plugin to have
21 * access to some common tools and utilities provided by Bacula
22 */
23
24 #ifndef PCOMMON_H
25 #define PCOMMON_H
26
27 #define JT_BACKUP 'B' /* Backup Job */
28 #define JT_RESTORE 'R' /* Restore Job */
29
30 #define L_FULL 'F' /* Full backup */
31 #define L_INCREMENTAL 'I' /* since last backup */
32 #define L_DIFFERENTIAL 'D' /* since last full backup */
33
34 #ifndef DLL_IMP_EXP
35 # if defined(BUILDING_DLL)
36 # define DLL_IMP_EXP __declspec(dllexport)
37 # elif defined(USING_DLL)
38 # define DLL_IMP_EXP __declspec(dllimport)
39 # else
40 # define DLL_IMP_EXP
41 # endif
42 #endif
43
44 #ifdef SMARTALLOC
45 DLL_IMP_EXP void *sm_malloc(const char *fname, int lineno, unsigned int nbytes);
46 DLL_IMP_EXP void sm_free(const char *file, int line, void *fp);
47 DLL_IMP_EXP void *reallymalloc(const char *fname, int lineno, unsigned int nbytes);
48 DLL_IMP_EXP void reallyfree(const char *file, int line, void *fp);
49
50 #ifndef bmalloc
51 # define bmalloc(s) sm_malloc(__FILE__, __LINE__, (s))
52 # define bfree(o) sm_free(__FILE__, __LINE__, (o))
53 #endif
54
55 #define SM_CHECK sm_check(__FILE__, __LINE__, false)
56
57 #ifdef malloc
58 #undef malloc
59 #undef free
60 #endif
61
62 #define malloc(s) sm_malloc(__FILE__, __LINE__, (s))
63 #define free(o) sm_free(__FILE__, __LINE__, (o))
64
65 /* Looks to be broken on scientific linux */
66 #ifdef xxxx
new(size_t size,char const * file,int line)67 inline void *operator new(size_t size, char const * file, int line)
68 {
69 void *pnew = sm_malloc(file,line, size);
70 memset((char *)pnew, 0, size);
71 return pnew;
72 }
73
74 inline void *operator new[](size_t size, char const * file, int line)
75 {
76 void *pnew = sm_malloc(file, line, size);
77 memset((char *)pnew, 0, size);
78 return pnew;
79 }
80
new(size_t size)81 inline void *operator new(size_t size)
82 {
83 void *pnew = sm_malloc(__FILE__, __LINE__, size);
84 memset((char *)pnew, 0, size);
85 return pnew;
86 }
87
88 inline void *operator new[](size_t size)
89 {
90 void *pnew = sm_malloc(__FILE__, __LINE__, size);
91 memset((char *)pnew, 0, size);
92 return pnew;
93 }
94
95 #define new new(__FILE__, __LINE__)
96
delete(void * buf)97 inline void operator delete(void *buf)
98 {
99 sm_free( __FILE__, __LINE__, buf);
100 }
101
102 inline void operator delete[] (void *buf)
103 {
104 sm_free(__FILE__, __LINE__, buf);
105 }
106
107 inline void operator delete[] (void *buf, char const * file, int line)
108 {
109 sm_free(file, line, buf);
110 }
111
delete(void * buf,char const * file,int line)112 inline void operator delete(void *buf, char const * file, int line)
113 {
114 sm_free(file, line, buf);
115 }
116
117 #endif
118 #endif /* !SMARTALLOC */
119
120 #define Dmsg(context, level, ...) bfuncs->DebugMessage(context, __FILE__, __LINE__, level, __VA_ARGS__ )
121 #define Jmsg(context, type, ...) bfuncs->JobMessage(context, __FILE__, __LINE__, type, 0, __VA_ARGS__ )
122
123
124 #ifdef USE_CMD_PARSER
125 #include "lib/cmd_parser.h"
126 #endif /* USE_CMD_PARSER */
127
128 #ifdef USE_ADD_DRIVE
129 /* Keep drive letters for windows vss snapshot */
add_drive(char * drives,int * nCount,char * fname)130 static void add_drive(char *drives, int *nCount, char *fname) {
131 if (strlen(fname) >= 2 && B_ISALPHA(fname[0]) && fname[1] == ':') {
132 /* always add in uppercase */
133 char ch = toupper(fname[0]);
134 /* if not found in string, add drive letter */
135 if (!strchr(drives,ch)) {
136 drives[*nCount] = ch;
137 drives[*nCount+1] = 0;
138 (*nCount)++;
139 }
140 }
141 }
142
143 /* Copy our drive list to Bacula core list */
copy_drives(char * drives,char * dest)144 static void copy_drives(char *drives, char *dest) {
145 int last = strlen(dest); /* dest is 27 bytes long */
146 for (char *p = drives; *p && last < 26; p++) {
147 if (!strchr(dest, *p)) {
148 dest[last++] = *p;
149 dest[last] = 0;
150 }
151 }
152 }
153 #endif /* USE_ADD_DRIVE */
154
155 #endif /* ! PCOMMON_H */
156
157 #ifdef USE_JOB_LIST
158
159 /* This class is used to store locally the job history, you can attach data
160 * to it such as snapshot names
161 * !!! Don't forget that this file may be deleted by the user. !!!
162 */
163
164 class joblist: public SMARTALLOC
165 {
166 private:
167 bpContext *ctx;
168
169 public:
170 char level; /* level of the job */
171
172 char base[MAX_NAME_LENGTH]; /* base name */
173 char key[MAX_NAME_LENGTH]; /* group of backup */
174 char name[MAX_NAME_LENGTH]; /* job name */
175 char prev[MAX_NAME_LENGTH]; /* based on jobname */
176 char root[MAX_NAME_LENGTH]; /* root of this branch */
177 char rootdiff[MAX_NAME_LENGTH]; /* root of diff if any */
178
179 btime_t job_time; /* job time */
180
init()181 void init() {
182 level = 0;
183 job_time = 0;
184 *key = *name = *prev = *root = *rootdiff = 0;
185 set_base("jobs.dat");
186 ctx = NULL;
187 }
188
set_base(const char * b)189 void set_base(const char *b) {
190 strncpy(base, b, sizeof(base));
191 }
192
joblist(bpContext * actx)193 joblist(bpContext *actx) { init(); ctx = actx; }
194
joblist(bpContext * actx,const char * akey,const char * jobname,const char * prevjobname,char joblevel)195 joblist(bpContext *actx,
196 const char *akey,
197 const char *jobname,
198 const char *prevjobname,
199 char joblevel)
200 {
201 init();
202 ctx = actx;
203 if (jobname) {
204 strncpy(name, jobname, MAX_NAME_LENGTH);
205 }
206
207 if (prevjobname) {
208 strncpy(prev, prevjobname, MAX_NAME_LENGTH);
209 }
210
211 level = joblevel;
212
213 if (akey) {
214 strncpy(key, akey, MAX_NAME_LENGTH);
215
216 } else {
217 get_key_from_name();
218 }
219 }
220
~joblist()221 ~joblist() { }
222
223 /* Will extract the name from the full job name */
get_key_from_name()224 bool get_key_from_name() {
225 // pluginTest.2012-07-19_16.59.21_11
226 int l = strlen(name);
227 int dlen = 23; // strlen(".2012-07-19_16.59.21_11");
228
229 if (l > dlen) { /* we probably have a key */
230 int start = l - dlen;
231 if (name[start] == '.' &&
232 B_ISDIGIT(name[start + 1]) && // 2
233 B_ISDIGIT(name[start + 2]) && // 0
234 B_ISDIGIT(name[start + 3]) && // 1
235 B_ISDIGIT(name[start + 4]) && // 2
236 name[start + 5] == '-' && // -
237 B_ISDIGIT(name[start + 6]) && // 0
238 B_ISDIGIT(name[start + 7])) // 7
239 {
240 bstrncpy(key, name, start + 1);
241 Dmsg(ctx, dbglvl+100, "key is %s from jobname %s\n", key, name);
242 return true;
243 }
244 }
245 Dmsg(ctx, dbglvl+100, "Unable to get key from jobname %s\n", name);
246 return false;
247 }
248
249 bool find_job(const char *name, POOLMEM **data=NULL); /* set root, job_time */
250 bool find_root_job();
251 void store_job(char *data);
252 void prune_jobs(char *build_cmd(void *arg, const char *data, const char *job),
253 void *arg, alist *jobs);
254 };
255
256 static pthread_mutex_t joblist_mutex = PTHREAD_MUTEX_INITIALIZER;
257
find_job(const char * name,POOLMEM ** data)258 bool joblist::find_job(const char *name, POOLMEM **data)
259 {
260 BFILE fp;
261 FILE *f;
262 POOLMEM *tmp;
263 char buf[1024];
264 char curkey[MAX_NAME_LENGTH]; /* key */
265 char curjobname[MAX_NAME_LENGTH]; /* jobname */
266 char prevjob[MAX_NAME_LENGTH]; /* last jobname */
267 char rootjob[MAX_NAME_LENGTH]; /* root jobname */
268 char t[MAX_NAME_LENGTH];
269 char curlevel;
270 bool ok=false;
271
272 *root = 0;
273 job_time = 0;
274 *rootdiff = 0;
275
276 binit(&fp);
277 set_portable_backup(&fp);
278
279 tmp = get_pool_memory(PM_FNAME);
280 Mmsg(tmp, "%s/%s", working, base);
281
282 P(joblist_mutex);
283 if (bopen(&fp, tmp, O_RDONLY, 0) < 0) {
284 berrno be;
285 Jmsg(ctx, M_ERROR, "Unable to open job database %s for reading. ERR=%s\n",
286 tmp, be.bstrerror(errno));
287 goto bail_out;
288 }
289
290 f = fdopen(fp.fid, "r");
291 if (!f) {
292 berrno be;
293 Jmsg(ctx, M_ERROR, "Unable to open job database. ERR=%s\n",
294 be.bstrerror(errno));
295 goto bail_out;
296 }
297
298 while (!ok && fgets(buf, sizeof(buf), f) != NULL) {
299 *curkey = *curjobname = *rootjob = *prevjob = 0;
300
301 Dmsg(ctx, dbglvl+100, "line = [%s]\n", buf);
302
303 if (sscanf(buf, "time=%60s level=%c key=%127s name=%127s root=%127s prev=%127s",
304 t, &curlevel, curkey, curjobname, rootjob, prevjob) != 6) {
305
306 if (sscanf(buf, "time=%60s level=F key=%127s name=%127s",
307 t, curkey, curjobname) != 3) {
308 Dmsg(ctx, dbglvl+100, "Bad line l=[%s]\n", buf);
309 continue;
310 }
311 }
312
313 if (strcmp(name, curjobname) == 0 &&
314 strcmp(key, curkey) == 0)
315 {
316 job_time = str_to_uint64(t);
317 bstrncpy(root, rootjob, MAX_NAME_LENGTH);
318 if (curlevel == 'D') {
319 bstrncpy(rootdiff, curjobname, MAX_NAME_LENGTH);
320 }
321
322 if (data) {
323 pm_strcpy(data, strstr(buf, " vol=") + 5);
324 strip_trailing_newline(*data);
325 unbash_spaces(*data);
326 }
327
328 ok = true;
329 Dmsg(ctx, dbglvl+100, "Found job root %s -> %s -> %s\n",
330 rootdiff, root, curjobname);
331 }
332 }
333
334 fclose(f);
335
336 bail_out:
337 V(joblist_mutex);
338 free_pool_memory(tmp);
339 return ok;
340
341 }
342
343 /* Find the root job for the current job */
find_root_job()344 bool joblist::find_root_job()
345 {
346 BFILE fp;
347 FILE *f;
348 POOLMEM *tmp;
349 char buf[1024];
350 char curkey[MAX_NAME_LENGTH]; /* key */
351 char curjobname[MAX_NAME_LENGTH]; /* jobname */
352 char prevjob[MAX_NAME_LENGTH]; /* last jobname */
353 char rootjob[MAX_NAME_LENGTH]; /* root jobname */
354 char t[MAX_NAME_LENGTH];
355 char curlevel;
356 bool ok=false;
357
358 *root = 0;
359 job_time = 0;
360
361 if (level == 'F') {
362 bstrncpy(root, name, MAX_NAME_LENGTH);
363 return true;
364 }
365
366 binit(&fp);
367 set_portable_backup(&fp);
368
369 tmp = get_pool_memory(PM_FNAME);
370 Mmsg(tmp, "%s/%s", working, base);
371
372 P(joblist_mutex);
373 if (bopen(&fp, tmp, O_RDONLY, 0) < 0) {
374 berrno be;
375 Jmsg(ctx, M_ERROR, "Unable to prune previous jobs. "
376 "Can't open %s for reading ERR=%s\n",
377 tmp, be.bstrerror(errno));
378 goto bail_out;
379 }
380
381 f = fdopen(fp.fid, "r");
382 if (!f) {
383 berrno be;
384 Jmsg(ctx, M_ERROR, "Unable to prune previous jobs. ERR=%s\n",
385 be.bstrerror(errno));
386 goto bail_out;
387 }
388
389 while (!ok && fgets(buf, sizeof(buf), f) != NULL) {
390 *curkey = *curjobname = *rootjob = *prevjob = 0;
391
392 Dmsg(ctx, dbglvl+100, "line = [%s]\n", buf);
393
394 if (sscanf(buf, "time=%60s level=%c key=%127s name=%127s root=%127s prev=%127s",
395 t, &curlevel, curkey, curjobname, rootjob, prevjob) != 6) {
396
397 if (sscanf(buf, "time=%60s level=F key=%127s name=%127s",
398 t, curkey, curjobname) == 3) {
399 bstrncpy(rootjob, curjobname, MAX_NAME_LENGTH);
400 *prevjob = 0;
401 curlevel = 'F';
402
403 } else {
404 Dmsg(ctx, dbglvl+100, "Bad line l=[%s]\n", buf);
405 continue;
406 }
407 }
408
409 if (strcmp(key, curkey) == 0 &&
410 strcmp(prev, curjobname) == 0)
411 {
412 bstrncpy(root, rootjob, MAX_NAME_LENGTH);
413
414 if (curlevel == 'D') {
415 bstrncpy(rootdiff, curjobname, MAX_NAME_LENGTH);
416 }
417 ok = true;
418 Dmsg(ctx, dbglvl+100, "Found job root %s -> %s -> %s\n",
419 rootdiff, root, curjobname);
420 }
421 }
422
423 fclose(f);
424
425 bail_out:
426 V(joblist_mutex);
427 free_pool_memory(tmp);
428 return true;
429 }
430
431 /* Store the current job in the jobs.dat for a specific data list */
store_job(char * data)432 void joblist::store_job(char *data)
433 {
434 BFILE fp;
435 int l;
436 POOLMEM *tmp = NULL;
437 btime_t now;
438
439 /* Not initialized, no need to store jobs */
440 if (*name == 0 || !level) {
441 Dmsg(ctx, dbglvl+100, "store_job fail name=%s level=%d\n", name, level);
442 return;
443 }
444
445 find_root_job();
446
447 binit(&fp);
448 set_portable_backup(&fp);
449
450 P(joblist_mutex);
451
452 tmp = get_pool_memory(PM_FNAME);
453 Mmsg(tmp, "%s/%s", working, base);
454 if (bopen(&fp, tmp, O_WRONLY|O_CREAT|O_APPEND, 0600) < 0) {
455 berrno be;
456 Jmsg(ctx, M_ERROR, "Unable to update the job history. ERR=%s\n",
457 be.bstrerror(errno));
458 goto bail_out;
459 }
460
461 now = time(NULL);
462
463 bash_spaces(data);
464
465 if (level == 'F') {
466 l = Mmsg(tmp, "time=%lld level=%c key=%s name=%s vollen=%d vol=%s\n",
467 now, level, key, name, strlen(data), data);
468
469 } else {
470 l = Mmsg(tmp, "time=%lld level=%c key=%s name=%s root=%s prev=%s vollen=%d vol=%s\n",
471 now, level, key, name, root, prev, strlen(data), data);
472 }
473
474 if (bwrite(&fp, tmp, l) != l) {
475 berrno be;
476 Jmsg(ctx, M_ERROR, "Unable to update the job history. ERR=%s\n",
477 be.bstrerror(errno));
478 }
479
480 bclose(&fp);
481
482 bail_out:
483 V(joblist_mutex);
484 free_pool_memory(tmp);
485 }
486
487 /* Prune jobs at the end of the job, this function can generate commands
488 * in order to cleanup something
489 */
prune_jobs(char * build_cmd (void * arg,const char * data,const char * job),void * arg,alist * jobs)490 void joblist::prune_jobs(char *build_cmd(void *arg, const char *data, const char *job),
491 void *arg, alist *jobs)
492 {
493 BFILE fp, fpout;
494 FILE *f=NULL;
495 POOLMEM *tmp;
496 POOLMEM *tmpout;
497 POOLMEM *data;
498 POOLMEM *buf;
499 char curkey[MAX_NAME_LENGTH]; /* key */
500 char jobname[MAX_NAME_LENGTH]; /* jobname */
501 char prevjob[MAX_NAME_LENGTH]; /* last jobname */
502 char rootjob[MAX_NAME_LENGTH]; /* root jobname */
503 char t[MAX_NAME_LENGTH];
504 uint32_t datalen;
505 char curlevel;
506 bool keep;
507 bool ok=false;
508 int count=0, len;
509
510 /* In Incremental, it means that the previous Full/Diff is well terminated */
511 if (level != 'I') {
512 return;
513 }
514
515 find_root_job();
516
517 binit(&fp);
518 set_portable_backup(&fp);
519
520 binit(&fpout);
521 set_portable_backup(&fpout);
522
523 tmp = get_pool_memory(PM_FNAME);
524 Mmsg(tmp, "%s/%s", working, base);
525
526 tmpout = get_pool_memory(PM_FNAME);
527 Mmsg(tmpout, "%s/%s.swap", working, base);
528
529 buf = get_pool_memory(PM_FNAME);
530 data = get_pool_memory(PM_FNAME);
531 *buf = *data = 0;
532
533 P(joblist_mutex);
534 if (bopen(&fp, tmp, O_RDONLY, 0) < 0) {
535 berrno be;
536 Jmsg(ctx, M_ERROR, "Unable to prune previous jobs. "
537 "Can't open %s for reading ERR=%s\n",
538 tmp, be.bstrerror(errno));
539 goto bail_out;
540 }
541 if (bopen(&fpout, tmpout, O_CREAT|O_WRONLY, 0600) < 0) {
542 berrno be;
543 Jmsg(ctx, M_ERROR, "Unable to prune previous jobs. "
544 "Can't open %s for writing ERR=%s\n",
545 tmpout, be.bstrerror(errno));
546 goto bail_out;
547 }
548
549 f = fdopen(fp.fid, "r"); /* we use fgets from open() */
550 if (!f) {
551 berrno be;
552 Jmsg(ctx, M_ERROR, "Unable to prune previous jobs. ERR=%s\n",
553 be.bstrerror(errno));
554 goto bail_out;
555 }
556
557 while (fgets(buf, sizeof_pool_memory(buf), f) != NULL) {
558 *data = *curkey = *jobname = *rootjob = *prevjob = 0;
559 keep = false;
560 datalen = 0;
561
562 len = strlen(buf);
563 if (len > 0 && buf[len -1] != '\n') {
564 /* The line is larger than the buffer, we need to capture the rest */
565 bool ok=false;
566 while (!ok) {
567 Dmsg(ctx, dbglvl+100, "Reading extra 1024 bytes, len=%d\n", len);
568 buf = check_pool_memory_size(buf, sizeof_pool_memory(buf) + 1024);
569 if (fgets(buf + len, 1023, f) == NULL) {
570 ok = true;
571 }
572 len = strlen(buf);
573 if (buf[len - 1] == '\n') {
574 ok = true;
575 }
576 if (len > 32000) { /* sanity check */
577 ok = true;
578 }
579 }
580 }
581
582 /* We don't capture the vol list, because our sscanf is limited to 1000 bytes */
583 if (sscanf(buf, "time=%60s level=%c key=%127s name=%127s root=%127s prev=%127s vollen=%d vol=",
584 t, &curlevel, curkey, jobname, rootjob, prevjob, &datalen) != 7) {
585
586 if (sscanf(buf, "time=%60s level=F key=%127s name=%127s vollen=%d vol=",
587 t, curkey, jobname, &datalen) == 4) {
588 *rootdiff = *rootjob = *prevjob = 0;
589 curlevel = 'F';
590
591 } else {
592 Dmsg(ctx, dbglvl+100, "Bad line l=[%s]\n", buf);
593 keep = true;
594 }
595 }
596
597 if (!keep) {
598 pm_strcpy(data, strstr(buf, " vol=") + 5);
599 strip_trailing_newline(data);
600 unbash_spaces(data);
601
602 if (datalen != strlen(data)) {
603 Dmsg(ctx, dbglvl+100, "Bad data line datalen != strlen(data) %d != %d\n", datalen, strlen(data));
604 Dmsg(ctx, dbglvl+100, "v=[%s]\n", data);
605 }
606 }
607
608
609 if (!keep &&
610 (strcmp(key, curkey) != 0 ||
611 strcmp(name, jobname) == 0 ||
612 strcmp(prev, jobname) == 0 ||
613 strcmp(root, jobname) == 0 ||
614 strcmp(rootdiff, jobname) == 0))
615 {
616 keep = true;
617 }
618
619 if (keep) {
620 if (bwrite(&fpout, buf, len) < 0) {
621 berrno be;
622 Jmsg(ctx, M_ERROR, "Unable to update the job history. ERR=%s\n",
623 be.bstrerror(errno));
624 goto bail_out;
625 }
626
627 } else if (build_cmd) {
628 count++;
629 Dmsg(ctx, dbglvl+100, "Can prune jobname %s\n", jobname);
630
631 char *p2 = data;
632 for(char *p = data; *p; p++) {
633 if (*p == ',') {
634 *p = 0;
635 jobs->append(bstrdup(build_cmd(arg, p2, jobname)));
636 p2 = p + 1 ;
637 }
638 }
639 jobs->append(bstrdup(build_cmd(arg, p2, jobname)));
640
641 } else if (jobs) {
642 jobs->append(bstrdup(data));
643 }
644 }
645
646 ok = true;
647
648 bail_out:
649 if (f) {
650 fclose(f);
651 }
652 if (is_bopen(&fpout)) {
653 bclose(&fpout);
654 }
655
656 /* We can switch the file */
657 if (ok) {
658 unlink(tmp);
659
660 if (rename(tmpout, tmp) < 0) {
661 berrno be;
662 Jmsg(ctx, M_ERROR, "Unable to update the job history. ERR=%s\n",
663 be.bstrerror(errno));
664 }
665 }
666
667 V(joblist_mutex);
668 free_pool_memory(tmp);
669 free_pool_memory(tmpout);
670 free_pool_memory(data);
671 free_pool_memory(buf);
672
673 Dmsg(ctx, dbglvl+100, "Pruning %d jobs\n", count);
674 }
675
676
677 #endif /* ! USE_JOB_LIST */
678
679 #ifdef USE_FULL_WRITE
680 static int32_t full_write(int fd, const char *ptr, int32_t nbytes, bool *canceled=NULL)
681 {
682 ssize_t nleft, nwritten;
683 nleft = nbytes;
684 while (nleft > 0 && (canceled == NULL || *canceled == false)) {
685 do {
686 errno = 0;
687 nwritten = write(fd, ptr, nleft);
688 } while (nwritten == -1 && errno == EINTR && (canceled == NULL || *canceled == false));
689
690 if (nwritten <= 0) {
691 return nwritten; /* error */
692 }
693 nleft -= nwritten;
694 ptr += nwritten;
695 }
696 return nbytes - nleft;
697 }
698 #endif
699
700 #ifdef USE_MAKEDIR
701 /* Skip leading slash(es) */
makedir(char * path,mode_t mode)702 static bool makedir(char *path, mode_t mode)
703 {
704 struct stat statp;
705 char *p = path;
706
707 while (IsPathSeparator(*p)) {
708 p++;
709 }
710 while ((p = first_path_separator(p))) {
711 char save_p;
712 save_p = *p;
713 *p = 0;
714 if (mkdir(path, mode) != 0) {
715 if (stat(path, &statp) != 0) {
716 *p = save_p;
717 return false;
718 } else if (!S_ISDIR(statp.st_mode)) {
719 *p = save_p;
720 return false;
721 }
722 }
723 *p = save_p;
724 while (IsPathSeparator(*p)) {
725 p++;
726 }
727 }
728 return true;
729 }
730 #endif
731