1 /* $NetBSD: file.c,v 1.2 2024/02/21 22:52:28 christos 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