/*
 * Copyright 2017 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#define G_LOG_DOMAIN "FuCommon"

#include "config.h"

#include <errno.h>
#include <glib/gstdio.h>

#ifdef _WIN32
#include <stdlib.h>
#endif

#include "fwupd-error.h"

#include "fu-common.h"
#include "fu-path.h"

static gboolean
fu_path_delete(const gchar *path, GError **error)
{
	g_autoptr(GFile) file = g_file_new_for_path(path);
	if (!g_file_delete(file, NULL, error)) {
		fwupd_error_convert(error);
		return FALSE;
	}
	return TRUE;
}

/**
 * fu_path_rmtree:
 * @directory: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Recursively removes a directory.
 *
 * Returns: %TRUE for success, %FALSE otherwise
 *
 * Since: 1.8.2
 **/
gboolean
fu_path_rmtree(const gchar *directory, GError **error)
{
	const gchar *filename;
	g_autoptr(GDir) dir = NULL;

	g_return_val_if_fail(directory != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* try to open */
	g_debug("removing %s", directory);
	dir = g_dir_open(directory, 0, error);
	if (dir == NULL)
		return FALSE;

	/* find each */
	while ((filename = g_dir_read_name(dir))) {
		g_autofree gchar *src = NULL;
		src = g_build_filename(directory, filename, NULL);
		if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
			if (!fu_path_rmtree(src, error))
				return FALSE;
		} else {
			if (!fu_path_delete(src, error))
				return FALSE;
		}
	}
	return fu_path_delete(directory, error);
}

static gboolean
fu_path_get_file_list_internal(GPtrArray *files, const gchar *directory, GError **error)
{
	const gchar *filename;
	g_autoptr(GDir) dir = NULL;

	/* try to open */
	dir = g_dir_open(directory, 0, error);
	if (dir == NULL) {
		fwupd_error_convert(error);
		return FALSE;
	}

	/* find each */
	while ((filename = g_dir_read_name(dir))) {
		g_autofree gchar *src = g_build_filename(directory, filename, NULL);
		if (g_file_test(src, G_FILE_TEST_IS_SYMLINK))
			continue;
		if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
			if (!fu_path_get_file_list_internal(files, src, error))
				return FALSE;
		} else {
			g_ptr_array_add(files, g_steal_pointer(&src));
		}
	}
	return TRUE;
}

/**
 * fu_path_get_files:
 * @path: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Returns every file found under @directory, and any subdirectory.
 * If any path under @directory cannot be accessed due to permissions an error
 * will be returned.
 *
 * Returns: (transfer container) (element-type utf8): array of files, or %NULL for error
 *
 * Since: 1.8.2
 **/
GPtrArray *
fu_path_get_files(const gchar *path, GError **error)
{
	g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);

	g_return_val_if_fail(path != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	if (!fu_path_get_file_list_internal(files, path, error))
		return NULL;
	return g_steal_pointer(&files);
}

/**
 * fu_path_mkdir:
 * @dirname: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Creates any required directories, including any parent directories.
 *
 * Returns: %TRUE for success
 *
 * Since: 1.8.2
 **/
gboolean
fu_path_mkdir(const gchar *dirname, GError **error)
{
	g_return_val_if_fail(dirname != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
		g_debug("creating path %s", dirname);
	if (g_mkdir_with_parents(dirname, 0755) == -1) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Failed to create '%s': %s",
			    dirname,
			    fwupd_strerror(errno));
		return FALSE;
	}
	return TRUE;
}

/**
 * fu_path_mkdir_parent:
 * @filename: a full pathname
 * @error: (nullable): optional return location for an error
 *
 * Creates any required directories, including any parent directories.
 *
 * Returns: %TRUE for success
 *
 * Since: 1.8.2
 **/
gboolean
fu_path_mkdir_parent(const gchar *filename, GError **error)
{
	g_autofree gchar *parent = NULL;

	g_return_val_if_fail(filename != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	parent = g_path_get_dirname(filename);
	return fu_path_mkdir(parent, error);
}

static gint
fu_path_glob_sort_cb(gconstpointer a, gconstpointer b)
{
	return g_strcmp0(*(const gchar **)a, *(const gchar **)b);
}

/**
 * fu_path_glob:
 * @directory: a directory path
 * @pattern: a glob pattern, e.g. `*foo*`
 * @error: (nullable): optional return location for an error
 *
 * Returns all the filenames that match a specific glob pattern.
 * Any results are sorted. No matching files will set @error.
 *
 * Returns:  (element-type utf8) (transfer container): matching files, or %NULL
 *
 * Since: 1.8.2
 **/
GPtrArray *
fu_path_glob(const gchar *directory, const gchar *pattern, GError **error)
{
	const gchar *basename;
	g_autoptr(GDir) dir = NULL;
	g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);

	g_return_val_if_fail(directory != NULL, NULL);
	g_return_val_if_fail(pattern != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	dir = g_dir_open(directory, 0, error);
	if (dir == NULL)
		return NULL;
	while ((basename = g_dir_read_name(dir)) != NULL) {
		if (!g_pattern_match_simple(pattern, basename))
			continue;
		g_ptr_array_add(files, g_build_filename(directory, basename, NULL));
	}
	if (files->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "no files matched pattern");
		return NULL;
	}
	g_ptr_array_sort(files, fu_path_glob_sort_cb);
	return g_steal_pointer(&files);
}

/**
 * fu_path_make_absolute:
 * @filename: a path to a filename, perhaps symlinked
 * @error: (nullable): optional return location for an error
 *
 * Returns the resolved absolute file name.
 *
 * Returns: (transfer full): path, or %NULL on error
 *
 * Since: 2.0.0
 **/
gchar *
fu_path_make_absolute(const gchar *filename, GError **error)
{
	char full_tmp[PATH_MAX];

	g_return_val_if_fail(filename != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

#ifdef HAVE_REALPATH
	if (realpath(filename, full_tmp) == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "cannot resolve path: %s",
			    fwupd_strerror(errno));
		return NULL;
	}
#else
	if (_fullpath(full_tmp, filename, sizeof(full_tmp)) == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "cannot resolve path: %s",
			    fwupd_strerror(errno));
		return NULL;
	}
#endif
	if (!g_file_test(full_tmp, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "cannot find path: %s",
			    full_tmp);
		return NULL;
	}
	return g_strdup(full_tmp);
}

/**
 * fu_path_get_symlink_target:
 * @filename: a path to a symlink
 * @error: (nullable): optional return location for an error
 *
 * Returns the symlink target.
 *
 * Returns: (transfer full): path, or %NULL on error
 *
 * Since: 2.0.0
 **/
gchar *
fu_path_get_symlink_target(const gchar *filename, GError **error)
{
	const gchar *target;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GFileInfo) info = NULL;

	file = g_file_new_for_path(filename);
	info = g_file_query_info(file,
				 G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
				 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
				 NULL,
				 error);
	if (info == NULL) {
		fwupd_error_convert(error);
		return NULL;
	}
	target =
	    g_file_info_get_attribute_byte_string(info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET);
	if (target == NULL) {
		g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no symlink target");
		return NULL;
	}

	/* success */
	return g_strdup(target);
}

/**
 * fu_path_verify_safe:
 * @filename: an optional local path and basename
 * @error: (nullable): optional return location for an error
 *
 * Verifies the path is safe to use from an archive.
 *
 * This will reject:
 * - an empty string
 * - absolute paths, e.g. `/etc/fstab`
 * - paths with relative locations, e.g. `../../etc/fstab`)
 * - paths with MS-DOS path separators, e.g. `foo\bar`
 * - non-ASCII filenames
 *
 * Returns: %TRUE on success
 *
 * Since: 2.1.2
 **/
gboolean
fu_path_verify_safe(const gchar *filename, GError **error)
{
	g_return_val_if_fail(filename != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* not ASCII */
	if (!g_str_is_ascii(filename)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "non-ASCII filenames not allowed");
		return FALSE;
	}

	/* not a basename */
	if (g_strcmp0(filename, "") == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "empty string not valid");
		return FALSE;
	}
	if (g_strcmp0(filename, ".") == 0 || g_strcmp0(filename, "..") == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "special paths not allowed");
		return FALSE;
	}

	/* absolute */
	if (g_path_is_absolute(filename)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "absolute paths not allowed");
		return FALSE;
	}

	/* relative */
	if (g_str_has_prefix(filename, "../") || g_strstr_len(filename, -1, "/../") != NULL ||
	    g_str_has_suffix(filename, "/..")) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "path traversal detected");
		return FALSE;
	}

	/* MS-DOS */
	if (g_strstr_len(filename, -1, "\\") != NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "MS-DOS path detected");
		return FALSE;
	}

	/* success */
	return TRUE;
}

/**
 * fu_path_sanitize_basename:
 * @str: a string to sanitize
 *
 * Sanitizes a string for use as a path basename by replacing path separators
 * and other dangerous characters with underscores.
 *
 * Returns: (transfer full): a newly allocated sanitized string
 *
 * Since: 2.1.4
 **/
gchar *
fu_path_sanitize_basename(const gchar *str)
{
	g_autoptr(GString) result = g_string_new(str);

	g_return_val_if_fail(str != NULL, NULL);

	/* replace dangerous characters */
	g_string_replace(result, "/", "_", 0);
	g_string_replace(result, "\\", "_", 0);
	g_string_replace(result, "..", "_", 0);

	/* detect hidden files */
	if (g_str_has_prefix(result->str, "."))
		result->str[0] = '_';

	return g_string_free(g_steal_pointer(&result), FALSE);
}
