/* libparted - a library for manipulating disk partitions
    Copyright (C) 1999-2001, 2007-2014, 2019-2023 Free Software Foundation,
    Inc.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/** \file filesys.c */

/**
 * \addtogroup PedFileSystem
 *
 * \note File systems exist on a PedGeometry - NOT a PedPartition.
 *
 * @{
 */

#include <config.h>

#include <parted/parted.h>
#include <parted/debug.h>
#include "pt-tools.h"

#if ENABLE_NLS
#  include <libintl.h>
#  define _(String) dgettext (PACKAGE, String)
#else
#  define _(String) (String)
#endif /* ENABLE_NLS */

#define STREQ(a, b) (strcmp (a, b) == 0)

#ifndef MIN
# define MIN(a,b) (((a) < (b)) ? (a) : (b))
#endif

typedef PedFileSystem * (*open_fn_t) (PedGeometry *);
extern PedFileSystem *hfsplus_open (PedGeometry *);
extern PedFileSystem *hfs_open (PedGeometry *);
extern PedFileSystem *fat_open (PedGeometry *);

typedef int (*close_fn_t) (PedFileSystem *);
extern int hfsplus_close (PedFileSystem *);
extern int hfs_close (PedFileSystem *);
extern int fat_close (PedFileSystem *);

typedef int (*resize_fn_t) (PedFileSystem *fs, PedGeometry *geom,
			    PedTimer *timer);
extern int hfsplus_resize (PedFileSystem *fs, PedGeometry *geom,
			   PedTimer *timer);
extern int hfs_resize (PedFileSystem *fs, PedGeometry *geom,
		       PedTimer *timer);
extern int fat_resize (PedFileSystem *fs, PedGeometry *geom,
		       PedTimer *timer);

typedef PedConstraint * (*resize_constraint_fn_t) (PedFileSystem const *fs);
extern PedConstraint *hfsplus_get_resize_constraint (PedFileSystem const *fs);
extern PedConstraint *hfs_get_resize_constraint (PedFileSystem const *fs);
extern PedConstraint *fat_get_resize_constraint (PedFileSystem const *fs);

static bool
is_hfs_plus (char const *fs_type_name)
{
  return STREQ (fs_type_name, "hfsx") || STREQ (fs_type_name, "hfs+");
}

static open_fn_t
open_fn (char const *fs_type_name)
{
  if (is_hfs_plus (fs_type_name))
    return hfsplus_open;
  if (STREQ (fs_type_name, "hfs"))
    return hfs_open;
  if (strncmp (fs_type_name, "fat", 3) == 0)
    return fat_open;
  return NULL;
}

static close_fn_t
close_fn (char const *fs_type_name)
{
  if (is_hfs_plus (fs_type_name))
    return hfsplus_close;
  if (STREQ (fs_type_name, "hfs"))
    return hfs_close;
  if (strncmp (fs_type_name, "fat", 3) == 0)
    return fat_close;
  return NULL;
}

static resize_fn_t
resize_fn (char const *fs_type_name)
{
  if (is_hfs_plus (fs_type_name))
    return hfsplus_resize;
  if (STREQ (fs_type_name, "hfs"))
    return hfs_resize;
  if (strncmp (fs_type_name, "fat", 3) == 0)
    return fat_resize;
  return NULL;
}

static resize_constraint_fn_t
resize_constraint_fn (char const *fs_type_name)
{
  if (is_hfs_plus (fs_type_name))
    return hfsplus_get_resize_constraint;
  if (STREQ (fs_type_name, "hfs"))
    return hfs_get_resize_constraint;
  if (strncmp (fs_type_name, "fat", 3) == 0)
    return fat_get_resize_constraint;
  return NULL;
}

/**
 * This function opens the file system stored on \p geom, if it
 * can find one.
 * It is often called in the following manner:
 * \code
 *     fs = ped_file_system_open (&part.geom)
 * \endcode
 *
 * \throws PED_EXCEPTION_ERROR if file system could not be detected
 * \throws PED_EXCEPTION_ERROR if the file system is bigger than its volume
 * \throws PED_EXCEPTION_NO_FEATURE if opening of a file system stored on
 *     \p geom is not implemented
 *
 * \return a PedFileSystem on success, \c NULL on failure.
 */
PedFileSystem *
ped_file_system_open (PedGeometry* geom)
{
       PED_ASSERT (geom != NULL);

       if (!ped_device_open (geom->dev))
               goto error;

       PedFileSystemType *type = ped_file_system_probe (geom);
       if (!type) {
               ped_exception_throw (PED_EXCEPTION_ERROR, PED_EXCEPTION_CANCEL,
                                    _("Could not detect file system."));
               goto error_close_dev;
       }

       open_fn_t open_f = open_fn (type->name);
       if (open_f == NULL) {
	   ped_exception_throw (PED_EXCEPTION_ERROR, PED_EXCEPTION_CANCEL,
				_("resizing %s file systems is not supported"),
				type->name);
	   goto error_close_dev;
       }

       PedGeometry *probed_geom = ped_file_system_probe_specific (type, geom);
       if (!probed_geom)
               goto error_close_dev;
       if (!ped_geometry_test_inside (geom, probed_geom)) {
               if (ped_exception_throw (
                       PED_EXCEPTION_ERROR,
                       PED_EXCEPTION_IGNORE_CANCEL,
                       _("The file system is bigger than its volume!"))
                               != PED_EXCEPTION_IGNORE)
                       goto error_destroy_probed_geom;
       }

       PedFileSystem *fs = (*open_f) (probed_geom);
       if (!fs)
               goto error_destroy_probed_geom;
       ped_geometry_destroy (probed_geom);
       fs->type = type;
       return fs;

error_destroy_probed_geom:
       ped_geometry_destroy (probed_geom);
error_close_dev:
       ped_device_close (geom->dev);
error:
       return NULL;
}

/**
 * Close file system \p fs.
 *
 * \return \c 1 on success, \c 0 on failure
 */
int
ped_file_system_close (PedFileSystem* fs)
{
       PED_ASSERT (fs != NULL);
       PedDevice *dev = fs->geom->dev;
       close_fn_t fn = close_fn (fs->type->name);

       if (!fn || !(fn (fs)))
               goto error_close_dev;
       ped_device_close (dev);
       return 1;

error_close_dev:
       ped_device_close (dev);
       return 0;
}

/**
 * This function erases all file system signatures that indicate that a
 * file system occupies a given region described by \p geom.
 * After this operation ped_file_system_probe() won't detect any file system.
 *
 * \return \c 1 on success, \c 0 on failure
 */
static int
ped_file_system_clobber (PedGeometry* geom)
{
  PED_ASSERT (geom != NULL);

  if (!ped_device_open (geom->dev))
    return 0;

  /* Clear the first three and the last two sectors, albeit fewer
     when GEOM is too small.  */
  PedSector len = MIN (geom->length, geom->dev->length);

  int ok = (len <= 5
	    ? ptt_geom_clear_sectors (geom, 0, len)
	    : (ptt_geom_clear_sectors (geom, 0, 3)
	       && ptt_geom_clear_sectors (geom, geom->dev->length - 2, 2)));

  ped_device_close (geom->dev);
  return !!ok;
}

/* This function erases all signatures that indicate the presence of
 * a file system in a particular region, without erasing any data
 * contained inside the "exclude" region.
 */
static int
ped_file_system_clobber_exclude (PedGeometry* geom,
				 const PedGeometry* exclude)
{
	PedGeometry*    clobber_geom;
	int             status;

	if (ped_geometry_test_sector_inside (exclude, geom->start))
		return 1;

	clobber_geom = ped_geometry_duplicate (geom);
	if (ped_geometry_test_overlap (clobber_geom, exclude))
		ped_geometry_set_end (clobber_geom, exclude->start - 1);

	status = ped_file_system_clobber (clobber_geom);
	ped_geometry_destroy (clobber_geom);
	return status;
}

/**
 * Resize \p fs to new geometry \p geom.
 *
 * \p geom should satisfy the ped_file_system_get_resize_constraint().
 * (This isn't asserted, so it's not a bug not to... just it's likely
 * to fail ;)  If \p timer is non-NULL, it is used as the progress meter.
 *
 * \throws PED_EXCEPTION_NO_FEATURE if resizing of file system \p fs
 *     is not implemented yet
 *
 * \return \c 0 on failure
 */
int
ped_file_system_resize (PedFileSystem *fs, PedGeometry *geom, PedTimer *timer)
{
       PED_ASSERT (fs != NULL);
       PED_ASSERT (geom != NULL);

       resize_fn_t resize_f = resize_fn (fs->type->name);
       if (resize_f == NULL) {
	   ped_exception_throw (PED_EXCEPTION_ERROR, PED_EXCEPTION_CANCEL,
				_("resizing %s file systems is not supported"),
				fs->type->name);
	   return 0;
       }

       if (!ped_file_system_clobber_exclude (geom, fs->geom))
               return 0;

       return resize_f (fs, geom, timer);
}

/**
 * Return a constraint that represents all of the possible ways the
 * file system \p fs can be resized with ped_file_system_resize().
 * This takes into account the amount of used space on
 * the filesystem \p fs and the capabilities of the resize algorithm.
 * Hints:
 * -# if constraint->start_align->grain_size == 0, or
 *    constraint->start_geom->length == 1, then the start cannot be moved
 * -# constraint->min_size is the minimum size you can resize the partition
 *    to.  You might want to tell the user this ;-).
 *
 * \return a PedConstraint on success, \c NULL on failure
 */
PedConstraint *
ped_file_system_get_resize_constraint (const PedFileSystem *fs)
{
	PED_ASSERT (fs != NULL);

	resize_constraint_fn_t resize_constraint_f =
	  resize_constraint_fn (fs->type->name);
	if (resize_constraint_f == NULL)
	  return NULL;

	return resize_constraint_f (fs);
}
