Home | History | Annotate | Download | only in bart
      1 /*
      2  * CDDL HEADER START
      3  *
      4  * The contents of this file are subject to the terms of the
      5  * Common Development and Distribution License (the "License").
      6  * You may not use this file except in compliance with the License.
      7  *
      8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
      9  * or http://www.opensolaris.org/os/licensing.
     10  * See the License for the specific language governing permissions
     11  * and limitations under the License.
     12  *
     13  * When distributing Covered Code, include this CDDL HEADER in each
     14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     15  * If applicable, add the following below this CDDL HEADER, with the
     16  * fields enclosed by brackets "[]" replaced with your own identifying
     17  * information: Portions Copyright [yyyy] [name of copyright owner]
     18  *
     19  * CDDL HEADER END
     20  */
     21 /*
     22  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
     23  * Use is subject to license terms.
     24  */
     25 
     26 #include <signal.h>
     27 #include <unistd.h>
     28 #include <sys/acl.h>
     29 #include <sys/statvfs.h>
     30 #include <sys/wait.h>
     31 #include "bart.h"
     32 #include <aclutils.h>
     33 
     34 static int	sanitize_reloc_root(char *root, size_t bufsize);
     35 static int	create_manifest_filelist(char **argv, char *reloc_root);
     36 static int	create_manifest_rule(char *reloc_root, FILE *rule_fp);
     37 static void	output_manifest(void);
     38 static int	eval_file(const char *fname, const struct stat64 *statb);
     39 static char	*sanitized_fname(const char *, boolean_t);
     40 static char	*get_acl_string(const char *fname, const struct stat64 *statb,
     41     int *err_code);
     42 static int	generate_hash(int fdin, char *hash_str);
     43 static int	read_filelist(char *reloc_root, char **argv, char *buf,
     44     size_t bufsize);
     45 static int	walker(const char *name, const struct stat64 *sp,
     46     int type, struct FTW *ftwx);
     47 
     48 /*
     49  * The following globals are necessary due to the "walker" function
     50  * provided by nftw().  Since there is no way to pass them through to the
     51  * walker function, they must be global.
     52  */
     53 static int		compute_chksum = 1, eval_err = 0;
     54 static struct rule	*subtree_root;
     55 static char		reloc_root[PATH_MAX];
     56 static struct statvfs64	parent_vfs;
     57 
     58 int
     59 bart_create(int argc, char **argv)
     60 {
     61 	boolean_t	filelist_input;
     62 	int		ret, c, output_pipe[2];
     63 	FILE 		*rules_fd = NULL;
     64 	pid_t		pid;
     65 
     66 	filelist_input = B_FALSE;
     67 	reloc_root[0] = '\0';
     68 
     69 	while ((c = getopt(argc, argv, "Inr:R:")) != EOF) {
     70 		switch (c) {
     71 		case 'I':
     72 			if (rules_fd != NULL) {
     73 				(void) fprintf(stderr, "%s", INPUT_ERR);
     74 				usage();
     75 			}
     76 			filelist_input = B_TRUE;
     77 			break;
     78 
     79 		case 'n':
     80 			compute_chksum = 0;
     81 			break;
     82 
     83 		case 'r':
     84 			if (strcmp(optarg, "-") == 0)
     85 				rules_fd = stdin;
     86 			else
     87 				rules_fd = fopen(optarg, "r");
     88 			if (rules_fd == NULL) {
     89 				perror(optarg);
     90 				usage();
     91 			}
     92 			break;
     93 
     94 		case 'R':
     95 			(void) strlcpy(reloc_root, optarg, sizeof (reloc_root));
     96 			ret = sanitize_reloc_root(reloc_root,
     97 			    sizeof (reloc_root));
     98 			if (ret == 0)
     99 				usage();
    100 			break;
    101 
    102 		case '?':
    103 		default :
    104 			usage();
    105 		}
    106 	}
    107 	argv += optind;
    108 
    109 	if (pipe(output_pipe) < 0) {
    110 		perror("");
    111 		exit(FATAL_EXIT);
    112 	}
    113 
    114 	pid = fork();
    115 	if (pid < 0) {
    116 		perror(NULL);
    117 		exit(FATAL_EXIT);
    118 	}
    119 
    120 	/*
    121 	 * Break the creation of a manifest into two parts: the parent process
    122 	 * generated the data whereas the child process sorts the data.
    123 	 *
    124 	 * The processes communicate through the pipe.
    125 	 */
    126 	if (pid > 0) {
    127 		/*
    128 		 * Redirect the stdout of this process so it goes into
    129 		 * output_pipe[0].  The output of this process will be read
    130 		 * by the child, which will sort the output.
    131 		 */
    132 		if (dup2(output_pipe[0], STDOUT_FILENO) != STDOUT_FILENO) {
    133 			perror(NULL);
    134 			exit(FATAL_EXIT);
    135 		}
    136 		(void) close(output_pipe[0]);
    137 		(void) close(output_pipe[1]);
    138 
    139 		if (filelist_input == B_TRUE) {
    140 			ret = create_manifest_filelist(argv, reloc_root);
    141 		} else {
    142 			ret = create_manifest_rule(reloc_root, rules_fd);
    143 		}
    144 
    145 		/* Close stdout so the sort in the child proc will complete */
    146 		(void) fclose(stdout);
    147 	} else {
    148 		/*
    149 		 * Redirect the stdin of this process so its read in from
    150 		 * the pipe, which is the parent process in this case.
    151 		 */
    152 		if (dup2(output_pipe[1], STDIN_FILENO) != STDIN_FILENO) {
    153 			perror(NULL);
    154 			exit(FATAL_EXIT);
    155 		}
    156 		(void) close(output_pipe[0]);
    157 
    158 		output_manifest();
    159 	}
    160 
    161 	/* Wait for the child proc (the sort) to complete */
    162 	(void) wait(0);
    163 
    164 	return (ret);
    165 }
    166 
    167 /*
    168  * Handle the -R option and sets 'root' to be the absolute path of the
    169  * relocatable root.  This is useful when the user specifies '-R ../../foo'.
    170  *
    171  * Return code is whether or not the location spec'd by the -R flag is a
    172  * directory or not.
    173  */
    174 static int
    175 sanitize_reloc_root(char *root, size_t bufsize)
    176 {
    177 	char		pwd[PATH_MAX];
    178 
    179 	/*
    180 	 * First, save the current directory and go to the location
    181 	 * specified with the -R option.
    182 	 */
    183 	(void) getcwd(pwd, sizeof (pwd));
    184 	if (chdir(root) < 0) {
    185 		/* Failed to change directory, something is wrong.... */
    186 		perror(root);
    187 		return (0);
    188 	}
    189 
    190 	/*
    191 	 * Save the absolute path of the relocatable root directory.
    192 	 */
    193 	(void) getcwd(root, bufsize);
    194 
    195 	/*
    196 	 * Now, go back to where we started, necessary for picking up a rules
    197 	 * file.
    198 	 */
    199 	if (chdir(pwd) < 0) {
    200 		/* Failed to change directory, something is wrong.... */
    201 		perror(root);
    202 		return (0);
    203 	}
    204 
    205 	/*
    206 	 * Make sure the path returned does not have a trailing /. This
    207 	 * can only happen when the entire pathname is "/".
    208 	 */
    209 	if (strcmp(root, "/") == 0)
    210 		root[0] = '\0';
    211 
    212 	/*
    213 	 * Since the earlier chdir() succeeded, return success.
    214 	 */
    215 	return (1);
    216 }
    217 
    218 /*
    219  * This is the worker bee which creates the manifest based upon the command
    220  * line options supplied by the user.
    221  *
    222  * NOTE: create_manifest() eventually outputs data to a pipe, which is read in
    223  * by the child process.  The child process is running output_manifest(), which
    224  * is responsible for generating sorted output.
    225  */
    226 static int
    227 create_manifest_rule(char *reloc_root, FILE *rule_fp)
    228 {
    229 	struct rule	*root;
    230 	int		ret_status = EXIT;
    231 	uint_t		flags;
    232 
    233 	if (compute_chksum)
    234 		flags = ATTR_CONTENTS;
    235 	else
    236 		flags = 0;
    237 	ret_status = read_rules(rule_fp, reloc_root, flags, 1);
    238 
    239 	/* Loop through every single subtree */
    240 	for (root = get_first_subtree(); root != NULL;
    241 	    root = get_next_subtree(root)) {
    242 
    243 		/*
    244 		 * Check to see if this subtree should have contents
    245 		 * checking turned on or off.
    246 		 *
    247 		 * NOTE: The 'compute_chksum' and 'parent_vfs'
    248 		 * are a necessary hack: the variables are used in
    249 		 * walker(), both directly and indirectly.  Since
    250 		 * the parameters to walker() are defined by nftw(),
    251 		 * the globals are really a backdoor mechanism.
    252 		 */
    253 		ret_status = statvfs64(root->subtree, &parent_vfs);
    254 		if (ret_status < 0) {
    255 			perror(root->subtree);
    256 			continue;
    257 		}
    258 
    259 		/*
    260 		 * Walk the subtree and invoke the callback function
    261 		 * walker()
    262 		 */
    263 		subtree_root = root;
    264 		(void) nftw64(root->subtree, &walker, 20, FTW_PHYS);
    265 
    266 		/*
    267 		 * Ugly but necessary:
    268 		 *
    269 		 * walker() must return 0, or the tree walk will stop,
    270 		 * so warning flags must be set through a global.
    271 		 */
    272 		if (eval_err == WARNING_EXIT)
    273 			ret_status = WARNING_EXIT;
    274 
    275 	}
    276 	return (ret_status);
    277 }
    278 
    279 static int
    280 create_manifest_filelist(char **argv, char *reloc_root)
    281 {
    282 	int	ret_status = EXIT;
    283 	char	input_fname[PATH_MAX];
    284 
    285 	while (read_filelist(reloc_root, argv,
    286 	    input_fname, sizeof (input_fname)) != -1) {
    287 
    288 		struct stat64	stat_buf;
    289 		int		ret;
    290 
    291 		ret = lstat64(input_fname, &stat_buf);
    292 		if (ret < 0) {
    293 			ret_status = WARNING_EXIT;
    294 			perror(input_fname);
    295 		} else {
    296 			ret = eval_file(input_fname, &stat_buf);
    297 
    298 			if (ret == WARNING_EXIT)
    299 				ret_status = WARNING_EXIT;
    300 		}
    301 	}
    302 
    303 	return (ret_status);
    304 }
    305 
    306 /*
    307  * output_manifest() the child process.  It reads in the output from
    308  * create_manifest() and sorts it.
    309  */
    310 static void
    311 output_manifest(void)
    312 {
    313 	char	*env[] = {"LC_CTYPE=C", "LC_COLLATE=C", "LC_NUMERIC=C", NULL};
    314 	time_t		time_val;
    315 	struct tm	*tm;
    316 	char		time_buf[1024];
    317 
    318 	(void) printf("%s", MANIFEST_VER);
    319 	time_val = time((time_t)0);
    320 	tm = localtime(&time_val);
    321 	(void) strftime(time_buf, sizeof (time_buf), "%A, %B %d, %Y (%T)", tm);
    322 	(void) printf("! %s\n", time_buf);
    323 	(void) printf("%s", FORMAT_STR);
    324 	(void) fflush(stdout);
    325 	/*
    326 	 * Simply run sort and read from the the current stdin, which is really
    327 	 * the output of create_manifest().
    328 	 * Also, make sure the output is unique, since a given file may be
    329 	 * included by several stanzas.
    330 	 */
    331 	if (execle("/usr/bin/sort", "sort", "-u", NULL, env) < 0) {
    332 		perror("");
    333 		exit(FATAL_EXIT);
    334 	}
    335 
    336 	/*NOTREACHED*/
    337 }
    338 
    339 /*
    340  * Callback function for nftw()
    341  */
    342 static int
    343 walker(const char *name, const struct stat64 *sp, int type, struct FTW *ftwx)
    344 {
    345 	int			ret;
    346 	struct statvfs64	path_vfs;
    347 	boolean_t		dir_flag = B_FALSE;
    348 	struct rule		*rule;
    349 
    350 	switch (type) {
    351 	case FTW_F:	/* file 		*/
    352 		rule = check_rules(name, 'F');
    353 		if (rule != NULL) {
    354 			if (rule->attr_list & ATTR_CONTENTS)
    355 				compute_chksum = 1;
    356 			else
    357 				compute_chksum = 0;
    358 		}
    359 		break;
    360 	case FTW_SL:	/* symbolic link	*/
    361 	case FTW_DP:	/* end of directory	*/
    362 	case FTW_DNR:	/* unreadable directory	*/
    363 	case FTW_NS:	/* unstatable file	*/
    364 		break;
    365 	case FTW_D:	/* enter directory 		*/
    366 		dir_flag = B_TRUE;
    367 		ret = statvfs64(name, &path_vfs);
    368 		if (ret < 0)
    369 			eval_err = WARNING_EXIT;
    370 		break;
    371 	default:
    372 		(void) fprintf(stderr, INVALID_FILE, name);
    373 		eval_err = WARNING_EXIT;
    374 		break;
    375 	}
    376 
    377 	/* This is the function which really processes the file */
    378 	ret = eval_file(name, sp);
    379 
    380 	/*
    381 	 * Since the parameters to walker() are constrained by nftw(),
    382 	 * need to use a global to reflect a WARNING.  Sigh.
    383 	 */
    384 	if (ret == WARNING_EXIT)
    385 		eval_err = WARNING_EXIT;
    386 
    387 	/*
    388 	 * This is a case of a directory which crosses into a mounted
    389 	 * filesystem of a different type, e.g., UFS -> NFS.
    390 	 * BART should not walk the new filesystem (by specification), so
    391 	 * set this consolidation-private flag so the rest of the subtree
    392 	 * under this directory is not waled.
    393 	 */
    394 	if (dir_flag &&
    395 	    (strcmp(parent_vfs.f_basetype, path_vfs.f_basetype) != 0))
    396 		ftwx->quit = FTW_PRUNE;
    397 
    398 	return (0);
    399 }
    400 
    401 /*
    402  * This file does the per-file evaluation and is run to generate every entry
    403  * in the manifest.
    404  *
    405  * All output is written to a pipe which is read by the child process,
    406  * which is running output_manifest().
    407  */
    408 static int
    409 eval_file(const char *fname, const struct stat64 *statb)
    410 {
    411 	int	fd, ret, err_code, i;
    412 	char	last_field[PATH_MAX], ftype, *acl_str;
    413 	char	*quoted_name;
    414 
    415 	err_code = EXIT;
    416 
    417 	switch (statb->st_mode & S_IFMT) {
    418 	/* Regular file */
    419 	case S_IFREG: ftype = 'F'; break;
    420 
    421 	/* Directory */
    422 	case S_IFDIR: ftype = 'D'; break;
    423 
    424 	/* Block Device */
    425 	case S_IFBLK: ftype = 'B'; break;
    426 
    427 	/* Character Device */
    428 	case S_IFCHR: ftype = 'C'; break;
    429 
    430 	/* Named Pipe */
    431 	case S_IFIFO: ftype = 'P'; break;
    432 
    433 	/* Socket */
    434 	case S_IFSOCK: ftype = 'S'; break;
    435 
    436 	/* Door */
    437 	case S_IFDOOR: ftype = 'O'; break;
    438 
    439 	/* Symbolic link */
    440 	case S_IFLNK: ftype = 'L'; break;
    441 
    442 	default: ftype = '-'; break;
    443 	}
    444 
    445 	/* First, make sure this file should be cataloged */
    446 
    447 	if ((subtree_root != NULL) &&
    448 	    (exclude_fname(fname, ftype, subtree_root)))
    449 		return (err_code);
    450 
    451 	for (i = 0; i < PATH_MAX; i++)
    452 		last_field[i] = '\0';
    453 
    454 	/*
    455 	 * Regular files, compute the MD5 checksum and put it into 'last_field'
    456 	 * UNLESS instructed to ignore the checksums.
    457 	 */
    458 	if (ftype == 'F') {
    459 		if (compute_chksum) {
    460 			fd = open(fname, O_RDONLY|O_LARGEFILE);
    461 			if (fd < 0) {
    462 				err_code = WARNING_EXIT;
    463 				perror(fname);
    464 
    465 				/* default value since the computution failed */
    466 				(void) strcpy(last_field, "-");
    467 			} else {
    468 				if (generate_hash(fd, last_field) != 0) {
    469 					err_code = WARNING_EXIT;
    470 					(void) fprintf(stderr, CONTENTS_WARN,
    471 					    fname);
    472 					(void) strcpy(last_field, "-");
    473 				}
    474 			}
    475 			(void) close(fd);
    476 		}
    477 		/* Instructed to ignore checksums, just put in a '-' */
    478 		else
    479 			(void) strcpy(last_field, "-");
    480 	}
    481 
    482 	/*
    483 	 * For symbolic links, put the destination of the symbolic link into
    484 	 * 'last_field'
    485 	 */
    486 	if (ftype == 'L') {
    487 		ret = readlink(fname, last_field, sizeof (last_field));
    488 		if (ret < 0) {
    489 			err_code = WARNING_EXIT;
    490 			perror(fname);
    491 
    492 			/* default value since the computation failed */
    493 			(void) strcpy(last_field, "-");
    494 		}
    495 		else
    496 			(void) strlcpy(last_field,
    497 			    sanitized_fname(last_field, B_FALSE),
    498 			    sizeof (last_field));
    499 
    500 		/*
    501 		 * Boundary condition: possible for a symlink to point to
    502 		 * nothing [ ln -s '' link_name ].  For this case, set the
    503 		 * destination to "\000".
    504 		 */
    505 		if (strlen(last_field) == 0)
    506 			(void) strcpy(last_field, "\\000");
    507 	}
    508 
    509 	acl_str = get_acl_string(fname, statb, &err_code);
    510 
    511 	/* Sanitize 'fname', so its in the proper format for the manifest */
    512 	quoted_name = sanitized_fname(fname, B_TRUE);
    513 
    514 	/* Start to build the entry.... */
    515 	(void) printf("%s %c %d %o %s %x %d %d", quoted_name, ftype,
    516 	    (int)statb->st_size, (int)statb->st_mode, acl_str,
    517 	    (int)statb->st_mtime, (int)statb->st_uid, (int)statb->st_gid);
    518 
    519 	/* Finish it off based upon whether or not it's a device node */
    520 	if ((ftype == 'B') || (ftype == 'C'))
    521 		(void) printf(" %x\n", (int)statb->st_rdev);
    522 	else if (strlen(last_field) > 0)
    523 		(void) printf(" %s\n", last_field);
    524 	else
    525 		(void) printf("\n");
    526 
    527 	/* free the memory consumed */
    528 	free(acl_str);
    529 	free(quoted_name);
    530 
    531 	return (err_code);
    532 }
    533 
    534 /*
    535  * When creating a manifest, make sure all '?', tabs, space, newline, '/'
    536  * and '[' are all properly quoted.  Convert them to a "\ooo" where the 'ooo'
    537  * represents their octal value. For filesystem objects, as opposed to symlink
    538  * targets, also canonicalize the pathname.
    539  */
    540 static char *
    541 sanitized_fname(const char *fname, boolean_t canon_path)
    542 {
    543 	const char *ip;
    544 	unsigned char ch;
    545 	char *op, *quoted_name;
    546 
    547 	/* Initialize everything */
    548 	quoted_name = safe_calloc((4 * PATH_MAX) + 1);
    549 	ip = fname;
    550 	op = quoted_name;
    551 
    552 	if (canon_path) {
    553 		/*
    554 		 * In the case when a relocatable root was used, the relocatable
    555 		 * root should *not* be part of the manifest.
    556 		 */
    557 		ip += strlen(reloc_root);
    558 
    559 		/*
    560 		 * In the case when the '-I' option was used, make sure
    561 		 * the quoted_name starts with a '/'.
    562 		 */
    563 		if (*ip != '/')
    564 			*op++ = '/';
    565 	}
    566 
    567 	/* Now walk through 'fname' and build the quoted string */
    568 	while ((ch = *ip++) != 0) {
    569 		switch (ch) {
    570 		/* Quote the following characters */
    571 		case ' ':
    572 		case '*':
    573 		case '\n':
    574 		case '?':
    575 		case '[':
    576 		case '\\':
    577 		case '\t':
    578 			op += sprintf(op, "\\%.3o", (unsigned char)ch);
    579 			break;
    580 
    581 		/* Otherwise, simply append them */
    582 		default:
    583 			*op++ = ch;
    584 			break;
    585 		}
    586 	}
    587 
    588 	*op = 0;
    589 
    590 	return (quoted_name);
    591 }
    592 
    593 /*
    594  * Function responsible for generating the ACL information for a given
    595  * file.  Note, the string is put into buffer malloc'd by this function.
    596  * It's the responsibility of the caller to free the buffer.  This function
    597  * should never return a NULL pointer.
    598  */
    599 static char *
    600 get_acl_string(const char *fname, const struct stat64 *statb, int *err_code)
    601 {
    602 	acl_t		*aclp;
    603 	char		*acltext;
    604 	int		error;
    605 
    606 	if (S_ISLNK(statb->st_mode)) {
    607 		return (safe_strdup("-"));
    608 	}
    609 
    610 	/*
    611 	 *  Include trivial acl's
    612 	 */
    613 	error = acl_get(fname, 0, &aclp);
    614 
    615 	if (error != 0) {
    616 		*err_code = WARNING_EXIT;
    617 		(void) fprintf(stderr, "%s: %s\n", fname, acl_strerror(error));
    618 		return (safe_strdup("-"));
    619 	} else {
    620 		acltext = acl_totext(aclp, 0);
    621 		acl_free(aclp);
    622 		if (acltext == NULL)
    623 			return (safe_strdup("-"));
    624 		else
    625 			return (acltext);
    626 	}
    627 }
    628 
    629 
    630 /*
    631  *
    632  * description:	This routine reads stdin in BUF_SIZE chunks, uses the bits
    633  *		to update the md5 hash buffer, and outputs the chunks
    634  *		to stdout.  When stdin is exhausted, the hash is computed,
    635  *		converted to a hexadecimal string, and returned.
    636  *
    637  * returns:	The md5 hash of stdin, or NULL if unsuccessful for any reason.
    638  */
    639 static int
    640 generate_hash(int fdin, char *hash_str)
    641 {
    642 	unsigned char buf[BUF_SIZE];
    643 	unsigned char hash[MD5_DIGEST_LENGTH];
    644 	int i, amtread;
    645 	MD5_CTX ctx;
    646 
    647 	MD5Init(&ctx);
    648 
    649 	for (;;) {
    650 		amtread = read(fdin, buf, sizeof (buf));
    651 		if (amtread == 0)
    652 			break;
    653 		if (amtread <  0)
    654 			return (1);
    655 
    656 		/* got some data.  Now update hash */
    657 		MD5Update(&ctx, buf, amtread);
    658 	}
    659 
    660 	/* done passing through data, calculate hash */
    661 	MD5Final(hash, &ctx);
    662 
    663 	for (i = 0; i < MD5_DIGEST_LENGTH; i++)
    664 		(void) sprintf(hash_str + (i*2), "%2.2x", hash[i]);
    665 
    666 	return (0);
    667 }
    668 
    669 /*
    670  * Used by 'bart create' with the '-I' option.  Return each entry into a 'buf'
    671  * with the appropriate exit code: '0' for success and '-1' for failure.
    672  */
    673 static int
    674 read_filelist(char *reloc_root, char **argv, char *buf, size_t bufsize)
    675 {
    676 	static int		argv_index = -1;
    677 	static boolean_t	read_stdinput = B_FALSE;
    678 	char			temp_buf[PATH_MAX];
    679 	char 			*cp;
    680 
    681 	/*
    682 	 * INITIALIZATION:
    683 	 * Setup this code so it knows whether or not to read sdtin.
    684 	 * Also, if reading from argv, setup the index, "argv_index"
    685 	 */
    686 	if (argv_index == -1) {
    687 		argv_index = 0;
    688 
    689 		/* In this case, no args after '-I', so read stdin */
    690 		if (argv[0] == NULL)
    691 			read_stdinput = B_TRUE;
    692 	}
    693 
    694 	buf[0] = '\0';
    695 
    696 	if (read_stdinput) {
    697 		if (fgets(temp_buf, PATH_MAX, stdin) == NULL)
    698 			return (-1);
    699 		cp = strtok(temp_buf, "\n");
    700 	} else {
    701 		cp = argv[argv_index++];
    702 	}
    703 
    704 	if (cp == NULL)
    705 		return (-1);
    706 
    707 	/*
    708 	 * Unlike similar code elsewhere, avoid adding a leading
    709 	 * slash for relative pathnames.
    710 	 */
    711 	(void) snprintf(buf, bufsize,
    712 	    (reloc_root[0] == '\0' || cp[0] == '/') ? "%s%s" : "%s/%s",
    713 	    reloc_root, cp);
    714 
    715 	return (0);
    716 }
    717