/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 */

/*
 * The primary role of this file is to obtain a list of manifests that are
 * located in a specified directory or one of its subdirectories.  The
 * find_manifests() function provides this service, and
 * free_manifest_array() is used to free the memory associated with the
 * returned list.
 *
 * The find_manifests() function can return an array consisting of all the
 * .xml files in the directory and its subdirectories.  Alternatively,
 * find_manifests() can be asked to only return new manifests based on the
 * return of mhash_test_file().  The list that is returned is an array of
 * pointers to manifest_info structures.
 *
 * Implementation Notes:
 * ====================
 * This module makes use of the nftw(3C) function to scan the directory.
 * nftw() calls a processing function for every file that it finds.
 * Unfortunately, nftw does not allow us to pass in any structure pointers
 * to the processing function, and that makes it hard to accumulate a list.
 * Thus, we will use the thread specific data area to hold data that must
 * be retained between calls to the processing function.  This will allow
 * this module to be used in multi-threaded applications if the need
 * arises.
 */

#include <assert.h>
#include <errno.h>
#include <ftw.h>
#include <libscf.h>
#include <libuutil.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include "manifest_find.h"
#include "manifest_hash.h"

#define	MAX_DEPTH	24

/* Thread specific data */
typedef struct mftsd {
	manifest_info_t ** tsd_array;	/* Array of manifest_info structs */
	int		tsd_count;	/* Number items in list */
	int		tsd_max;	/* Number of pointers allocated */
					/* at tsd_array. */
	int		tsd_flags;	/* Check flags for hash and extension */
	scf_handle_t	*tsd_hndl;	/* Handle for libscf. */
} mftsd_t;

static pthread_key_t tsd_key = PTHREAD_ONCE_KEY_NP;

/*
 * Add the manifest info consisting of filename (fn), hash property name
 * (pname) and hash to the array at tsd_array.  If necessary, realloc()
 * will be called to increase the size of the buffer at tsd_array.
 *
 * Returns 0 on success and -1 on failure.  If a failure occurs, errno will
 * be set.
 */
static int
add_pointer(mftsd_t *tsdp, const char *fn, const char *pname, uchar_t *hash)
{
	manifest_info_t *info;
	manifest_info_t **newblock;
	int new_max;

	if (tsdp->tsd_count >= (tsdp->tsd_max - 1)) {
		/* Need more memory. */
		new_max = (tsdp->tsd_max == 0) ? 16 : 2 * tsdp->tsd_max;
		newblock = realloc(tsdp->tsd_array,
		    new_max * sizeof (*tsdp->tsd_array));
		if (newblock == NULL)
			return (-1);
		tsdp->tsd_array = newblock;
		/* NULL terminate list in case allocations fail below. */
		*(tsdp->tsd_array + tsdp->tsd_count) = NULL;
		tsdp->tsd_max = new_max;
	}
	info = uu_zalloc(sizeof (*info));
	if (info == NULL) {
		errno = ENOMEM;
		return (-1);
	}
	info->mi_path = uu_strdup(fn);
	if (info->mi_path == NULL) {
		uu_free(info);
		errno = ENOMEM;
		return (-1);
	}
	info->mi_prop = pname;
	if (hash != NULL)
		(void) memcpy(info->mi_hash, hash, MHASH_SIZE);
	*(tsdp->tsd_array + tsdp->tsd_count) = info;
	tsdp->tsd_count++;

	/* NULL terminate the list. */
	*(tsdp->tsd_array + tsdp->tsd_count) = NULL;

	return (0);
}

/*
 * If necessary initialize the thread specific data key at tsd_key, and
 * allocate a mftsd_t structure to hold our thread specific data.  Upon
 * success, the address the thread specific data is returned.  On failure,
 * NULL is returned and errno is set.
 */
static mftsd_t *
get_thread_specific_data()
{
	mftsd_t *tsdp;

	if (pthread_key_create_once_np(&tsd_key, NULL) != 0)
		return (NULL);
	tsdp = (mftsd_t *)pthread_getspecific(tsd_key);
	if (tsdp == NULL) {
		/*
		 * First time for this thread.  We need to allocate memory
		 * for our thread specific data.
		 */
		tsdp = uu_zalloc(sizeof (*tsdp));
		if (tsdp == NULL) {
			errno = ENOMEM;
			return (NULL);
		}
		errno = pthread_setspecific(tsd_key, tsdp);
		if (errno != 0) {
			/*
			 * EINVAL means that our key is invalid, which
			 * would be a coding error.
			 */
			assert(errno != EINVAL);
			return (NULL);
		}
	}
	return (tsdp);
}

/*
 * This function is called by nftw(3C) every time that it finds an object
 * in a directory of interest.  If the object is a file, process() checks
 * to see if it is a service bundle file by insuring that it has a .xml
 * extension.
 *
 * If the file is a service bundle file, and the CHECKHASH flag is set process()
 * calls mhash_test_file() to see if it is a new bundle.  Bundle file data
 * for selected bundles is added to tsd_array in our thread specific data.
 *
 * Assume given file is a manifest unless BUNDLE_PROF flag is set to indicate
 * it's a profile. For profile bundles, call mhash_test_file() with the
 * appropriate argument.
 *
 * The CHECKEXT flag may be set if this was not a directory search request
 * but a single service bundle file check that was determined by the caller to
 * be found based not on the extension of the file.
 */
/*ARGSUSED*/
static int
process(const char *fn, const struct stat *sp, int ftw_type,
    struct FTW *ftws)
{
	int is_profile;
	char *suffix_match;
	uchar_t hash[MHASH_SIZE];
	char *pname;
	mftsd_t *tsdp;

	if (ftw_type != FTW_F)
		return (0);

	tsdp = get_thread_specific_data();
	if (tsdp == NULL)
		return (-1);

	/*
	 * Only check the extension on the file when
	 * requested.
	 */
	if (tsdp->tsd_flags & CHECKEXT) {
		suffix_match = strstr(fn, ".xml");
		if (suffix_match == NULL || strcmp(suffix_match, ".xml") != 0)
			return (0);
	}

	if (tsdp->tsd_flags & CHECKHASH) {
		is_profile = (tsdp->tsd_flags & BUNDLE_PROF) ? 1 : 0;
		if (mhash_test_file(tsdp->tsd_hndl, fn, is_profile, &pname,
		    hash) == MHASH_NEWFILE) {
			return (add_pointer(tsdp, fn, pname, hash));
		}
	} else {
		return (add_pointer(tsdp, fn, NULL, NULL));
	}

	return (0);
}

/*
 * This function returns a pointer to an array of manifest_info_t pointers.
 * There is one manifest_info_t pointer for each service bundle file in the
 * directory, dir, that satifies the selection criteria.  The array is
 * returned to arrayp.  The array will be terminated with a NULL pointer.
 * It is the responsibility of the caller to free the memory associated
 * with the array by calling free_manifest_array().
 *
 * flags :
 * 	0x1 - CHECKHASH - do the hash check and only return bundle
 * 	files that do not have a hash entry in the smf/manifest table
 * 	or the hash value has changed due to the bundle file having
 * 	been modified.  If not set then all service bundle files found
 * 	are returned, regardless of the hash status.
 *
 * 	0x2 - CHECKEXT - Check the extension of the file is .xml
 *
 * On success a count of the number of selected bundles is returned.
 * Note, however, that *arrayp will be set to NULL if the selection is
 * empty, and a count of 0 will be returned.  In the case of failure, -1
 * will be returned and errno will be set.
 */
int
find_manifests(const char *dir, manifest_info_t ***arrayp, int flags)
{
	mftsd_t *tsdp;
	int status = -1;
	int count;

	tsdp = get_thread_specific_data();
	if (tsdp == NULL)
		return (NULL);

	tsdp->tsd_flags = flags;

	/*
	 * Create a handle for use by mhast_test_file() if
	 * the flag is set to request hash checking be enabled.
	 */
	if (tsdp->tsd_flags & CHECKHASH) {
		tsdp->tsd_hndl = scf_handle_create(SCF_VERSION);
		if (tsdp->tsd_hndl == NULL) {
			if (scf_error() == SCF_ERROR_NO_MEMORY) {
				errno = ENOMEM;
			} else {
				errno = EINVAL;
			}
			goto out;
		}
		if (scf_handle_bind(tsdp->tsd_hndl) != SCF_SUCCESS) {
			if (scf_error() == SCF_ERROR_NO_RESOURCES) {
				errno = ENOMEM;
			} else {
				errno = EINVAL;
			}
			goto out;
		}
	}

	if (nftw(dir, process, MAX_DEPTH, FTW_MOUNT) == 0) {
		status = 0;
	}

out:
	if (tsdp->tsd_hndl != NULL) {
		(void) scf_handle_unbind(tsdp->tsd_hndl);
		(void) scf_handle_destroy(tsdp->tsd_hndl);
	}
	if (status == 0) {
		*arrayp = tsdp->tsd_array;
		count = tsdp->tsd_count;
	} else {
		*arrayp = NULL;
		free_manifest_array(tsdp->tsd_array);
		count = -1;
	}

	/* Reset thread specific data. */
	(void) memset(tsdp, 0, sizeof (*tsdp));

	return (count);
}

/*
 * Free the memory associated with the array of manifest_info structures.
 */
void
free_manifest_array(manifest_info_t **array)
{
	manifest_info_t **entry;
	manifest_info_t *info;

	if (array == NULL)
		return;

	for (entry = array; *entry != NULL; entry++) {
		info = *entry;
		uu_free((void *) info->mi_path);
		uu_free((void *) info->mi_prop);
		uu_free(info);
	}

	/*
	 * Array is allocated with realloc(3C), so it must be freed with
	 * free(3c) rather than uu_free().
	 */
	free(array);
}