/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <isc/errno.h>
#include <isc/glob.h>
#include <isc/print.h>
#include <isc/result.h>
#include <isc/types.h>
#include <isc/util.h>

#if HAVE_GLOB_H
#include <glob.h>
#elif defined(_WIN32)
#include <stdlib.h>
#include <windows.h>

#include <isc/list.h>
#define GLOB_ERR     0x0004 /* Return on error. */
#define GLOB_NOSPACE (-1)
#define GLOB_NOMATCH (-3)

/* custom glob implementation for windows */
static int
glob(const char *pattern, int flags, void *unused, glob_t *pglob);

static void
globfree(glob_t *pglob);

#else
#error "Required header missing: glob.h"
#endif

isc_result_t
isc_glob(const char *pattern, glob_t *pglob) {
	REQUIRE(pattern != NULL);
	REQUIRE(*pattern != '\0');
	REQUIRE(pglob != NULL);

	int rc = glob(pattern, GLOB_ERR, NULL, pglob);

	switch (rc) {
	case 0:
		return (ISC_R_SUCCESS);

	case GLOB_NOMATCH:
		return (ISC_R_FILENOTFOUND);

	case GLOB_NOSPACE:
		return (ISC_R_NOMEMORY);

	default:
		return (errno != 0 ? isc_errno_toresult(errno) : ISC_R_IOERROR);
	}
}

void
isc_globfree(glob_t *pglob) {
	REQUIRE(pglob != NULL);
	globfree(pglob);
}

#if defined(_WIN32)

typedef struct file_path file_path_t;

struct file_path {
	char *path;
	ISC_LINK(file_path_t) link;
};

typedef ISC_LIST(file_path_t) file_list_t;

/* map a winapi error to a convenient errno code */
static int
map_error(DWORD win_err_code) {
	switch (win_err_code) {
	case ERROR_FILE_NOT_FOUND:
	case ERROR_PATH_NOT_FOUND:
		return (GLOB_NOMATCH);
	case ERROR_ACCESS_DENIED:
		return (EACCES);
	case ERROR_NOT_ENOUGH_MEMORY:
		return (GLOB_NOSPACE);
	default:
		return (EIO);
	}
}

/* add file in directory dir, that matches glob expression
 * provided in function glob(), to the linked list fl */
static void
append_file(isc_mem_t *mctx, file_list_t *fl, const char *dir, const char *file,
	    size_t full_path_len) {
	file_path_t *fp = isc_mem_get(mctx, sizeof(file_path_t));
	fp->path = isc_mem_get(mctx, full_path_len + 1);
	_snprintf(fp->path, full_path_len + 1, "%s%s", dir, file);

	ISC_LINK_INIT(fp, link);
	ISC_LIST_PREPEND(*fl, fp, link);
}

/* sort files alphabetically case insensitive on windows */
static int
path_cmp(const void *path1, const void *path2) {
	return _stricmp((const char *)path1, (const char *)path2);
}

static int
glob(const char *pattern, int flags, void *unused, glob_t *pglob) {
	char path[MAX_PATH];
	WIN32_FIND_DATAA find_data;
	int ec;
	HANDLE hnd;

	REQUIRE(pattern != NULL);
	REQUIRE(pglob != NULL);

	UNUSED(flags);
	UNUSED(unused);

	pglob->mctx = NULL;
	pglob->gl_pathc = 0;
	pglob->gl_pathv = NULL;

	hnd = FindFirstFileA(pattern, &find_data);
	if (hnd == INVALID_HANDLE_VALUE) {
		return (map_error(GetLastError()));
	}

	path[MAX_PATH - 1] = 0;
	strncpy(path, pattern, MAX_PATH);
	if (path[MAX_PATH - 1] != 0) {
		errno = ENAMETOOLONG;
		goto fail;
	}

	// strip filename from path.
	size_t dir_len = strlen(path);
	while (dir_len > 0 && path[dir_len - 1] != '/' &&
	       path[dir_len - 1] != '\\') {
		dir_len--;
	}

	path[dir_len] = '\0';

	isc_mem_create(&pglob->mctx);
	pglob->reserved = isc_mem_get(pglob->mctx, sizeof(file_list_t));
	ISC_LIST_INIT(*(file_list_t *)pglob->reserved);

	size_t entries = 0;

	do {
		size_t file_len = strlen(find_data.cFileName);
		size_t full_path_len = dir_len + file_len;

		if (full_path_len > MAX_PATH) {
			errno = ENAMETOOLONG;
			goto fail;
		}

		append_file(pglob->mctx, (file_list_t *)pglob->reserved, path,
			    find_data.cFileName, full_path_len);

		entries++;
	} while (FindNextFileA(hnd, &find_data));

	FindClose(hnd);

	pglob->gl_pathv = isc_mem_get(pglob->mctx,
				      (entries + 1) * sizeof(char *));
	pglob->gl_pathv[entries] = NULL;
	pglob->gl_pathc = entries;

	file_list_t *fl = (file_list_t *)pglob->reserved;

	size_t e = 0;
	file_path_t *fp;
	for (fp = ISC_LIST_HEAD(*fl); fp != NULL; fp = ISC_LIST_NEXT(fp, link))
	{
		pglob->gl_pathv[e++] = fp->path;
	}

	qsort(pglob->gl_pathv, pglob->gl_pathc, sizeof(char *), path_cmp);

	return (0);

fail:
	ec = errno;

	FindClose(hnd);

	if (pglob->mctx) {
		globfree(pglob);
	}

	return ec;
}

void
globfree(glob_t *pglob) {
	REQUIRE(pglob != NULL);
	REQUIRE(pglob->mctx != NULL);

	/* first free memory used by char ** gl_pathv */
	if (pglob->gl_pathv) {
		isc_mem_put(pglob->mctx, pglob->gl_pathv,
			    (pglob->gl_pathc + 1) * sizeof(char *));
		pglob->gl_pathv = NULL;
	}

	file_list_t *fl = (file_list_t *)pglob->reserved;
	file_path_t *p, *next;

	/* next free each individual file path string + nodes in list */
	for (p = ISC_LIST_HEAD(*fl); p != NULL; p = next) {
		next = ISC_LIST_NEXT(p, link);
		isc_mem_put(pglob->mctx, p->path, strlen(p->path) + 1);
		isc_mem_put(pglob->mctx, p, sizeof(file_path_t));
	}

	/* free linked list of files */
	isc_mem_put(pglob->mctx, pglob->reserved, sizeof(file_list_t));
	pglob->reserved = NULL;
	pglob->gl_pathc = 0;
	pglob->gl_pathv = NULL;

	isc_mem_destroy(&pglob->mctx);
	pglob->mctx = NULL;
}

#endif /* defined(_WIN32) */
