file.c revision 1.2.4.2 1 /* $NetBSD: file.c,v 1.2.4.2 2024/02/29 12:34:59 martin Exp $ */
2
3 /*
4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
5 *
6 * SPDX-License-Identifier: MPL-2.0
7 *
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
11 *
12 * See the COPYRIGHT file distributed with this work for additional
13 * information regarding copyright ownership.
14 */
15
16 /*
17 * Portions Copyright (c) 1987, 1993
18 * The Regents of the University of California. All rights reserved.
19 *
20 * Redistribution and use in source and binary forms, with or without
21 * modification, are permitted provided that the following conditions
22 * are met:
23 * 1. Redistributions of source code must retain the above copyright
24 * notice, this list of conditions and the following disclaimer.
25 * 2. Redistributions in binary form must reproduce the above copyright
26 * notice, this list of conditions and the following disclaimer in the
27 * documentation and/or other materials provided with the distribution.
28 * 3. Neither the name of the University nor the names of its contributors
29 * may be used to endorse or promote products derived from this software
30 * without specific prior written permission.
31 *
32 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
33 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
34 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
36 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
37 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
38 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
39 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
40 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
41 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
42 * SUCH DAMAGE.
43 */
44
45 /*! \file */
46
47 #include <errno.h>
48 #include <fcntl.h>
49 #include <inttypes.h>
50 #include <limits.h>
51 #include <stdbool.h>
52 #include <stdlib.h>
53 #include <sys/stat.h>
54 #include <sys/time.h>
55 #include <time.h> /* Required for utimes on some platforms. */
56 #include <unistd.h> /* Required for mkstemp on NetBSD. */
57
58 #ifdef HAVE_SYS_MMAN_H
59 #include <sys/mman.h>
60 #endif /* ifdef HAVE_SYS_MMAN_H */
61
62 #include <isc/dir.h>
63 #include <isc/file.h>
64 #include <isc/log.h>
65 #include <isc/md.h>
66 #include <isc/mem.h>
67 #include <isc/print.h>
68 #include <isc/random.h>
69 #include <isc/string.h>
70 #include <isc/time.h>
71 #include <isc/util.h>
72
73 #include "errno2result.h"
74
75 /*
76 * XXXDCL As the API for accessing file statistics undoubtedly gets expanded,
77 * it might be good to provide a mechanism that allows for the results
78 * of a previous stat() to be used again without having to do another stat,
79 * such as perl's mechanism of using "_" in place of a file name to indicate
80 * that the results of the last stat should be used. But then you get into
81 * annoying MP issues. BTW, Win32 has stat().
82 */
83 static isc_result_t
84 file_stats(const char *file, struct stat *stats) {
85 isc_result_t result = ISC_R_SUCCESS;
86
87 REQUIRE(file != NULL);
88 REQUIRE(stats != NULL);
89
90 if (stat(file, stats) != 0) {
91 result = isc__errno2result(errno);
92 }
93
94 return (result);
95 }
96
97 static isc_result_t
98 fd_stats(int fd, struct stat *stats) {
99 isc_result_t result = ISC_R_SUCCESS;
100
101 REQUIRE(stats != NULL);
102
103 if (fstat(fd, stats) != 0) {
104 result = isc__errno2result(errno);
105 }
106
107 return (result);
108 }
109
110 isc_result_t
111 isc_file_getsizefd(int fd, off_t *size) {
112 isc_result_t result;
113 struct stat stats;
114
115 REQUIRE(size != NULL);
116
117 result = fd_stats(fd, &stats);
118
119 if (result == ISC_R_SUCCESS) {
120 *size = stats.st_size;
121 }
122
123 return (result);
124 }
125
126 isc_result_t
127 isc_file_mode(const char *file, mode_t *modep) {
128 isc_result_t result;
129 struct stat stats;
130
131 REQUIRE(modep != NULL);
132
133 result = file_stats(file, &stats);
134 if (result == ISC_R_SUCCESS) {
135 *modep = (stats.st_mode & 07777);
136 }
137
138 return (result);
139 }
140
141 isc_result_t
142 isc_file_getmodtime(const char *file, isc_time_t *modtime) {
143 isc_result_t result;
144 struct stat stats;
145
146 REQUIRE(file != NULL);
147 REQUIRE(modtime != NULL);
148
149 result = file_stats(file, &stats);
150
151 if (result == ISC_R_SUCCESS) {
152 #if defined(HAVE_STAT_NSEC)
153 isc_time_set(modtime, stats.st_mtime, stats.st_mtim.tv_nsec);
154 #else /* if defined(HAVE_STAT_NSEC) */
155 isc_time_set(modtime, stats.st_mtime, 0);
156 #endif /* if defined(HAVE_STAT_NSEC) */
157 }
158
159 return (result);
160 }
161
162 isc_result_t
163 isc_file_getsize(const char *file, off_t *size) {
164 isc_result_t result;
165 struct stat stats;
166
167 REQUIRE(file != NULL);
168 REQUIRE(size != NULL);
169
170 result = file_stats(file, &stats);
171
172 if (result == ISC_R_SUCCESS) {
173 *size = stats.st_size;
174 }
175
176 return (result);
177 }
178
179 isc_result_t
180 isc_file_settime(const char *file, isc_time_t *when) {
181 struct timeval times[2];
182
183 REQUIRE(file != NULL && when != NULL);
184
185 /*
186 * tv_sec is at least a 32 bit quantity on all platforms we're
187 * dealing with, but it is signed on most (all?) of them,
188 * so we need to make sure the high bit isn't set. This unfortunately
189 * loses when either:
190 * * tv_sec becomes a signed 64 bit integer but long is 32 bits
191 * and isc_time_seconds > LONG_MAX, or
192 * * isc_time_seconds is changed to be > 32 bits but long is 32 bits
193 * and isc_time_seconds has at least 33 significant bits.
194 */
195 times[0].tv_sec = times[1].tv_sec = (long)isc_time_seconds(when);
196
197 /*
198 * Here is the real check for the high bit being set.
199 */
200 if ((times[0].tv_sec &
201 (1ULL << (sizeof(times[0].tv_sec) * CHAR_BIT - 1))) != 0)
202 {
203 return (ISC_R_RANGE);
204 }
205
206 /*
207 * isc_time_nanoseconds guarantees a value that divided by 1000 will
208 * fit into the minimum possible size tv_usec field.
209 */
210 times[0].tv_usec = times[1].tv_usec =
211 (int32_t)(isc_time_nanoseconds(when) / 1000);
212
213 if (utimes(file, times) < 0) {
214 return (isc__errno2result(errno));
215 }
216
217 return (ISC_R_SUCCESS);
218 }
219
220 #undef TEMPLATE
221 #define TEMPLATE "tmp-XXXXXXXXXX" /*%< 14 characters. */
222
223 isc_result_t
224 isc_file_mktemplate(const char *path, char *buf, size_t buflen) {
225 return (isc_file_template(path, TEMPLATE, buf, buflen));
226 }
227
228 isc_result_t
229 isc_file_template(const char *path, const char *templet, char *buf,
230 size_t buflen) {
231 const char *s;
232
233 REQUIRE(templet != NULL);
234 REQUIRE(buf != NULL);
235
236 if (path == NULL) {
237 path = "";
238 }
239
240 s = strrchr(templet, '/');
241 if (s != NULL) {
242 templet = s + 1;
243 }
244
245 s = strrchr(path, '/');
246
247 if (s != NULL) {
248 size_t prefixlen = s - path + 1;
249 if ((prefixlen + strlen(templet) + 1) > buflen) {
250 return (ISC_R_NOSPACE);
251 }
252
253 /* Copy 'prefixlen' bytes and NUL terminate. */
254 strlcpy(buf, path, ISC_MIN(prefixlen + 1, buflen));
255 strlcat(buf, templet, buflen);
256 } else {
257 if ((strlen(templet) + 1) > buflen) {
258 return (ISC_R_NOSPACE);
259 }
260
261 strlcpy(buf, templet, buflen);
262 }
263
264 return (ISC_R_SUCCESS);
265 }
266
267 static const char alphnum[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv"
268 "wxyz0123456789";
269
270 isc_result_t
271 isc_file_renameunique(const char *file, char *templet) {
272 char *x;
273 char *cp;
274
275 REQUIRE(file != NULL);
276 REQUIRE(templet != NULL);
277
278 cp = templet;
279 while (*cp != '\0') {
280 cp++;
281 }
282 if (cp == templet) {
283 return (ISC_R_FAILURE);
284 }
285
286 x = cp--;
287 while (cp >= templet && *cp == 'X') {
288 *cp = alphnum[isc_random_uniform(sizeof(alphnum) - 1)];
289 x = cp--;
290 }
291 while (link(file, templet) == -1) {
292 if (errno != EEXIST) {
293 return (isc__errno2result(errno));
294 }
295 for (cp = x;;) {
296 const char *t;
297 if (*cp == '\0') {
298 return (ISC_R_FAILURE);
299 }
300 t = strchr(alphnum, *cp);
301 if (t == NULL || *++t == '\0') {
302 *cp++ = alphnum[0];
303 } else {
304 *cp = *t;
305 break;
306 }
307 }
308 }
309 if (unlink(file) < 0) {
310 if (errno != ENOENT) {
311 return (isc__errno2result(errno));
312 }
313 }
314 return (ISC_R_SUCCESS);
315 }
316
317 isc_result_t
318 isc_file_openunique(char *templet, FILE **fp) {
319 int mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
320 return (isc_file_openuniquemode(templet, mode, fp));
321 }
322
323 isc_result_t
324 isc_file_openuniqueprivate(char *templet, FILE **fp) {
325 int mode = S_IWUSR | S_IRUSR;
326 return (isc_file_openuniquemode(templet, mode, fp));
327 }
328
329 isc_result_t
330 isc_file_openuniquemode(char *templet, int mode, FILE **fp) {
331 int fd;
332 FILE *f;
333 isc_result_t result = ISC_R_SUCCESS;
334 char *x;
335 char *cp;
336
337 REQUIRE(templet != NULL);
338 REQUIRE(fp != NULL && *fp == NULL);
339
340 cp = templet;
341 while (*cp != '\0') {
342 cp++;
343 }
344 if (cp == templet) {
345 return (ISC_R_FAILURE);
346 }
347
348 x = cp--;
349 while (cp >= templet && *cp == 'X') {
350 *cp = alphnum[isc_random_uniform(sizeof(alphnum) - 1)];
351 x = cp--;
352 }
353
354 while ((fd = open(templet, O_RDWR | O_CREAT | O_EXCL, mode)) == -1) {
355 if (errno != EEXIST) {
356 return (isc__errno2result(errno));
357 }
358 for (cp = x;;) {
359 char *t;
360 if (*cp == '\0') {
361 return (ISC_R_FAILURE);
362 }
363 t = strchr(alphnum, *cp);
364 if (t == NULL || *++t == '\0') {
365 *cp++ = alphnum[0];
366 } else {
367 *cp = *t;
368 break;
369 }
370 }
371 }
372 f = fdopen(fd, "w+");
373 if (f == NULL) {
374 result = isc__errno2result(errno);
375 if (remove(templet) < 0) {
376 isc_log_write(isc_lctx, ISC_LOGCATEGORY_GENERAL,
377 ISC_LOGMODULE_FILE, ISC_LOG_ERROR,
378 "remove '%s': failed", templet);
379 }
380 (void)close(fd);
381 } else {
382 *fp = f;
383 }
384
385 return (result);
386 }
387
388 isc_result_t
389 isc_file_bopenunique(char *templet, FILE **fp) {
390 int mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
391 return (isc_file_openuniquemode(templet, mode, fp));
392 }
393
394 isc_result_t
395 isc_file_bopenuniqueprivate(char *templet, FILE **fp) {
396 int mode = S_IWUSR | S_IRUSR;
397 return (isc_file_openuniquemode(templet, mode, fp));
398 }
399
400 isc_result_t
401 isc_file_bopenuniquemode(char *templet, int mode, FILE **fp) {
402 return (isc_file_openuniquemode(templet, mode, fp));
403 }
404
405 isc_result_t
406 isc_file_remove(const char *filename) {
407 int r;
408
409 REQUIRE(filename != NULL);
410
411 r = unlink(filename);
412 if (r == 0) {
413 return (ISC_R_SUCCESS);
414 } else {
415 return (isc__errno2result(errno));
416 }
417 }
418
419 isc_result_t
420 isc_file_rename(const char *oldname, const char *newname) {
421 int r;
422
423 REQUIRE(oldname != NULL);
424 REQUIRE(newname != NULL);
425
426 r = rename(oldname, newname);
427 if (r == 0) {
428 return (ISC_R_SUCCESS);
429 } else {
430 return (isc__errno2result(errno));
431 }
432 }
433
434 bool
435 isc_file_exists(const char *pathname) {
436 struct stat stats;
437
438 REQUIRE(pathname != NULL);
439
440 return (file_stats(pathname, &stats) == ISC_R_SUCCESS);
441 }
442
443 isc_result_t
444 isc_file_isplainfile(const char *filename) {
445 /*
446 * This function returns success if filename is a plain file.
447 */
448 struct stat filestat;
449 memset(&filestat, 0, sizeof(struct stat));
450
451 if ((stat(filename, &filestat)) == -1) {
452 return (isc__errno2result(errno));
453 }
454
455 if (!S_ISREG(filestat.st_mode)) {
456 return (ISC_R_INVALIDFILE);
457 }
458
459 return (ISC_R_SUCCESS);
460 }
461
462 isc_result_t
463 isc_file_isplainfilefd(int fd) {
464 /*
465 * This function returns success if filename is a plain file.
466 */
467 struct stat filestat;
468 memset(&filestat, 0, sizeof(struct stat));
469
470 if ((fstat(fd, &filestat)) == -1) {
471 return (isc__errno2result(errno));
472 }
473
474 if (!S_ISREG(filestat.st_mode)) {
475 return (ISC_R_INVALIDFILE);
476 }
477
478 return (ISC_R_SUCCESS);
479 }
480
481 isc_result_t
482 isc_file_isdirectory(const char *filename) {
483 /*
484 * This function returns success if filename exists and is a
485 * directory.
486 */
487 struct stat filestat;
488 memset(&filestat, 0, sizeof(struct stat));
489
490 if ((stat(filename, &filestat)) == -1) {
491 return (isc__errno2result(errno));
492 }
493
494 if (!S_ISDIR(filestat.st_mode)) {
495 return (ISC_R_INVALIDFILE);
496 }
497
498 return (ISC_R_SUCCESS);
499 }
500
501 bool
502 isc_file_isabsolute(const char *filename) {
503 REQUIRE(filename != NULL);
504 return (filename[0] == '/');
505 }
506
507 bool
508 isc_file_iscurrentdir(const char *filename) {
509 REQUIRE(filename != NULL);
510 return (filename[0] == '.' && filename[1] == '\0');
511 }
512
513 bool
514 isc_file_ischdiridempotent(const char *filename) {
515 REQUIRE(filename != NULL);
516 if (isc_file_isabsolute(filename)) {
517 return (true);
518 }
519 if (isc_file_iscurrentdir(filename)) {
520 return (true);
521 }
522 return (false);
523 }
524
525 const char *
526 isc_file_basename(const char *filename) {
527 const char *s;
528
529 REQUIRE(filename != NULL);
530
531 s = strrchr(filename, '/');
532 if (s == NULL) {
533 return (filename);
534 }
535
536 return (s + 1);
537 }
538
539 isc_result_t
540 isc_file_progname(const char *filename, char *buf, size_t buflen) {
541 const char *base;
542 size_t len;
543
544 REQUIRE(filename != NULL);
545 REQUIRE(buf != NULL);
546
547 base = isc_file_basename(filename);
548 len = strlen(base) + 1;
549
550 if (len > buflen) {
551 return (ISC_R_NOSPACE);
552 }
553 memmove(buf, base, len);
554
555 return (ISC_R_SUCCESS);
556 }
557
558 /*
559 * Put the absolute name of the current directory into 'dirname', which is
560 * a buffer of at least 'length' characters. End the string with the
561 * appropriate path separator, such that the final product could be
562 * concatenated with a relative pathname to make a valid pathname string.
563 */
564 static isc_result_t
565 dir_current(char *dirname, size_t length) {
566 char *cwd;
567 isc_result_t result = ISC_R_SUCCESS;
568
569 REQUIRE(dirname != NULL);
570 REQUIRE(length > 0U);
571
572 cwd = getcwd(dirname, length);
573
574 if (cwd == NULL) {
575 if (errno == ERANGE) {
576 result = ISC_R_NOSPACE;
577 } else {
578 result = isc__errno2result(errno);
579 }
580 } else {
581 if (strlen(dirname) + 1 == length) {
582 result = ISC_R_NOSPACE;
583 } else if (dirname[1] != '\0') {
584 strlcat(dirname, "/", length);
585 }
586 }
587
588 return (result);
589 }
590
591 isc_result_t
592 isc_file_absolutepath(const char *filename, char *path, size_t pathlen) {
593 isc_result_t result;
594 result = dir_current(path, pathlen);
595 if (result != ISC_R_SUCCESS) {
596 return (result);
597 }
598 if (strlen(path) + strlen(filename) + 1 > pathlen) {
599 return (ISC_R_NOSPACE);
600 }
601 strlcat(path, filename, pathlen);
602 return (ISC_R_SUCCESS);
603 }
604
605 isc_result_t
606 isc_file_truncate(const char *filename, isc_offset_t size) {
607 isc_result_t result = ISC_R_SUCCESS;
608
609 if (truncate(filename, size) < 0) {
610 result = isc__errno2result(errno);
611 }
612 return (result);
613 }
614
615 isc_result_t
616 isc_file_safecreate(const char *filename, FILE **fp) {
617 isc_result_t result;
618 int flags;
619 struct stat sb;
620 FILE *f;
621 int fd;
622
623 REQUIRE(filename != NULL);
624 REQUIRE(fp != NULL && *fp == NULL);
625
626 result = file_stats(filename, &sb);
627 if (result == ISC_R_SUCCESS) {
628 if ((sb.st_mode & S_IFREG) == 0) {
629 return (ISC_R_INVALIDFILE);
630 }
631 flags = O_WRONLY | O_TRUNC;
632 } else if (result == ISC_R_FILENOTFOUND) {
633 flags = O_WRONLY | O_CREAT | O_EXCL;
634 } else {
635 return (result);
636 }
637
638 fd = open(filename, flags, S_IRUSR | S_IWUSR);
639 if (fd == -1) {
640 return (isc__errno2result(errno));
641 }
642
643 f = fdopen(fd, "w");
644 if (f == NULL) {
645 result = isc__errno2result(errno);
646 close(fd);
647 return (result);
648 }
649
650 *fp = f;
651 return (ISC_R_SUCCESS);
652 }
653
654 isc_result_t
655 isc_file_splitpath(isc_mem_t *mctx, const char *path, char **dirname,
656 char const **bname) {
657 char *dir;
658 const char *file, *slash;
659
660 if (path == NULL) {
661 return (ISC_R_INVALIDFILE);
662 }
663
664 slash = strrchr(path, '/');
665
666 if (slash == path) {
667 file = ++slash;
668 dir = isc_mem_strdup(mctx, "/");
669 } else if (slash != NULL) {
670 file = ++slash;
671 dir = isc_mem_allocate(mctx, slash - path);
672 strlcpy(dir, path, slash - path);
673 } else {
674 file = path;
675 dir = isc_mem_strdup(mctx, ".");
676 }
677
678 if (dir == NULL) {
679 return (ISC_R_NOMEMORY);
680 }
681
682 if (*file == '\0') {
683 isc_mem_free(mctx, dir);
684 return (ISC_R_INVALIDFILE);
685 }
686
687 *dirname = dir;
688 *bname = file;
689
690 return (ISC_R_SUCCESS);
691 }
692
693 #define DISALLOW "\\/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
694
695 static isc_result_t
696 digest2hex(unsigned char *digest, unsigned int digestlen, char *hash,
697 size_t hashlen) {
698 unsigned int i;
699 int ret;
700 for (i = 0; i < digestlen; i++) {
701 size_t left = hashlen - i * 2;
702 ret = snprintf(hash + i * 2, left, "%02x", digest[i]);
703 if (ret < 0 || (size_t)ret >= left) {
704 return (ISC_R_NOSPACE);
705 }
706 }
707 return (ISC_R_SUCCESS);
708 }
709
710 isc_result_t
711 isc_file_sanitize(const char *dir, const char *base, const char *ext,
712 char *path, size_t length) {
713 char buf[PATH_MAX];
714 unsigned char digest[ISC_MAX_MD_SIZE];
715 unsigned int digestlen;
716 char hash[ISC_MAX_MD_SIZE * 2 + 1];
717 size_t l = 0;
718 isc_result_t err;
719
720 REQUIRE(base != NULL);
721 REQUIRE(path != NULL);
722
723 l = strlen(base) + 1;
724
725 /*
726 * allow room for a full sha256 hash (64 chars
727 * plus null terminator)
728 */
729 if (l < 65U) {
730 l = 65;
731 }
732
733 if (dir != NULL) {
734 l += strlen(dir) + 1;
735 }
736 if (ext != NULL) {
737 l += strlen(ext) + 1;
738 }
739
740 if (l > length || l > (unsigned)PATH_MAX) {
741 return (ISC_R_NOSPACE);
742 }
743
744 /* Check whether the full-length SHA256 hash filename exists */
745 err = isc_md(ISC_MD_SHA256, (const unsigned char *)base, strlen(base),
746 digest, &digestlen);
747 if (err != ISC_R_SUCCESS) {
748 return (err);
749 }
750
751 err = digest2hex(digest, digestlen, hash, sizeof(hash));
752 if (err != ISC_R_SUCCESS) {
753 return (err);
754 }
755
756 snprintf(buf, sizeof(buf), "%s%s%s%s%s", dir != NULL ? dir : "",
757 dir != NULL ? "/" : "", hash, ext != NULL ? "." : "",
758 ext != NULL ? ext : "");
759 if (isc_file_exists(buf)) {
760 strlcpy(path, buf, length);
761 return (ISC_R_SUCCESS);
762 }
763
764 /* Check for a truncated SHA256 hash filename */
765 hash[16] = '\0';
766 snprintf(buf, sizeof(buf), "%s%s%s%s%s", dir != NULL ? dir : "",
767 dir != NULL ? "/" : "", hash, ext != NULL ? "." : "",
768 ext != NULL ? ext : "");
769 if (isc_file_exists(buf)) {
770 strlcpy(path, buf, length);
771 return (ISC_R_SUCCESS);
772 }
773
774 /*
775 * If neither hash filename already exists, then we'll use
776 * the original base name if it has no disallowed characters,
777 * or the truncated hash name if it does.
778 */
779 if (strpbrk(base, DISALLOW) != NULL) {
780 strlcpy(path, buf, length);
781 return (ISC_R_SUCCESS);
782 }
783
784 snprintf(buf, sizeof(buf), "%s%s%s%s%s", dir != NULL ? dir : "",
785 dir != NULL ? "/" : "", base, ext != NULL ? "." : "",
786 ext != NULL ? ext : "");
787 strlcpy(path, buf, length);
788 return (ISC_R_SUCCESS);
789 }
790
791 bool
792 isc_file_isdirwritable(const char *path) {
793 return (access(path, W_OK | X_OK) == 0);
794 }
795