/*
 * fsarchiver: Filesystem Archiver
 *
 * Copyright (C) 2008-2018 Francois Dupoux.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License v2 as published by the Free Software Foundation.
 *
 * 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.
 *
 * Homepage: http://www.fsarchiver.org
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <string.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <uuid.h>

#include "fsarchiver.h"
#include "dico.h"
#include "common.h"
#include "fs_xfs.h"
#include "filesys.h"
#include "strlist.h"
#include "error.h"

// POSIX Extended Regular Expression to match RHEL 7 kernel releases
#define RHEL7_KERNEL_ERE "[[:digit:]]+\\.el7(uek)?\\."

int xfs_check_compatibility(u64 compat, u64 ro_compat, u64 incompat, u64 log_incompat)
{
    int errors=0;

    msgprintf(MSG_DEBUG1, "xfs features: compat=[%ld]\n", (long)compat);
    msgprintf(MSG_DEBUG1, "xfs features: ro_compat=[%ld]\n", (long)ro_compat);
    msgprintf(MSG_DEBUG1, "xfs features: incompat=[%ld]\n", (long)incompat);
    msgprintf(MSG_DEBUG1, "xfs features: log_incompat=[%ld]\n", (long)log_incompat);

    // to preserve the filesystem attributes, fsa must know all the features including the COMPAT ones
    if (compat & ~FSA_XFS_FEATURE_COMPAT_SUPP)
        errors++;

    if (ro_compat & ~FSA_XFS_FEATURE_RO_COMPAT_SUPP)
        errors++;

    if (incompat & ~FSA_XFS_FEATURE_INCOMPAT_SUPP)
        errors++;

    if (log_incompat & ~FSA_XFS_FEATURE_LOG_INCOMPAT_SUPP)
        errors++;

    if (errors > 0)
    {   errprintf("this filesystem has XFS features which are not supported by this fsarchiver version.\n");
        return -1;
    }
    else
    {   return 0;
    }
}

int xfs_mkfs(cdico *d, char *partition, char *fsoptions, char *mkfslabel, char *mkfsuuid)
{
    char stdoutbuf[2048];
    char command[2048];
    char buffer[2048];
    char mkfsopts[2048];
    char xadmopts[2048];
    char uuid[64];
    u64 xfstoolsver;
    int exitst;
    u64 temp64;
    u64 xfsver;
    int x, y, z;
    int optval;

    u64 sb_features_compat=0;
    u64 sb_features_ro_compat=0;
    u64 sb_features_incompat=0;
    u64 sb_features_log_incompat=0;

    // ---- check that mkfs is installed and get its version
    if (exec_command(command, sizeof(command), NULL, stdoutbuf, sizeof(stdoutbuf), NULL, 0, "mkfs.xfs -V")!=0)
    {   errprintf("mkfs.xfs not found. please install xfsprogs on your system or check the PATH.\n");
        return -1;
    }
    x=y=z=0;
    sscanf(stdoutbuf, "mkfs.xfs version %d.%d.%d", &x, &y, &z);
    if (x==0 && y==0)
    {   errprintf("Can't parse mkfs.xfs version number: x=y=0\n");
        return -1;
    }
    xfstoolsver=PROGVER(x,y,z);
    msgprintf(MSG_VERB2, "Detected mkfs.xfs version %d.%d.%d\n", x, y, z);

    memset(mkfsopts, 0, sizeof(mkfsopts));
    memset(xadmopts, 0, sizeof(xadmopts));
    memset(uuid, 0, sizeof(uuid));

    if (strlen(mkfslabel) > 0)
        strlcatf(mkfsopts, sizeof(mkfsopts), " -L '%.12s' ", mkfslabel);
    else if (dico_get_string(d, 0, FSYSHEADKEY_FSLABEL, buffer, sizeof(buffer))==0 && strlen(buffer)>0)
        strlcatf(mkfsopts, sizeof(mkfsopts), " -L '%.12s' ", buffer);

    if ((dico_get_u64(d, 0, FSYSHEADKEY_FSXFSBLOCKSIZE, &temp64)==0) && (temp64%512==0) && (temp64>=512) && (temp64<=65536))
        strlcatf(mkfsopts, sizeof(mkfsopts), " -b size=%ld ", (long)temp64);

    // ---- get xfs features attributes from the archive
    dico_get_u64(d, 0, FSYSHEADKEY_FSXFSFEATURECOMPAT, &sb_features_compat);
    dico_get_u64(d, 0, FSYSHEADKEY_FSXFSFEATUREROCOMPAT, &sb_features_ro_compat);
    dico_get_u64(d, 0, FSYSHEADKEY_FSXFSFEATUREINCOMPAT, &sb_features_incompat);
    dico_get_u64(d, 0, FSYSHEADKEY_FSXFSFEATURELOGINCOMPAT, &sb_features_log_incompat);

    // ---- check fsarchiver is aware of all the filesystem features used on that filesystem
    if (xfs_check_compatibility(sb_features_compat, sb_features_ro_compat, sb_features_incompat, sb_features_log_incompat)!=0)
        return -1;

    // Preserve XFS V4 if the original filesystem was V4 or if it was saved with
    // fsarchiver <= 0.6.19 which does not store the version in the metadata:
    // create XFS V4 if unsure for better compatibility
    if ((dico_get_u64(d, 0, FSYSHEADKEY_FSXFSVERSION, &temp64)!=0) || (temp64==XFS_SB_VERSION_4))
        xfsver = XFS_SB_VERSION_4;
    else
        xfsver = XFS_SB_VERSION_5;

    // Unfortunately it is impossible to preserve XFS V5 UUID (stamped on every metadata
    // block) with mkfs.xfs < 4.3.0. Restoring with a new random UUID could prevent the
    // system from booting if this is a boot/root filesystem because grub/fstab often
    // use UUID to identify it. Hence it is much safer restore as XFS V4 in this case.
    // More details: https://github.com/fdupoux/fsarchiver/issues/4
    if ((xfsver==XFS_SB_VERSION_5) && (xfstoolsver < PROGVER(4,3,0)))
    {
        xfsver = XFS_SB_VERSION_4; // Do not preserve the XFS version
        msgprintf(MSG_FORCE,
            "It is impossible to restore this filesystem as XFS V5 and preserve its UUID\n"
            "with mkfs.xfs < 4.3.0. This filesystem will be restored as XFS V4 instead\n"
            "as this is a much safer option (preserving the UUID may be required on\n"
            "boot/root filesystems for the operating system to be able to start). If you\n"
            "really need XFS V5, please upgrade xfsprogs to version 4.3.0 or more recent\n"
            "and rerun this operation\n");
    }

    // Determine if the "crc" mkfs option should be enabled (checksum)
    // - checksum must be disabled if we want to recreate XFS V4 filesystem
    // - checksum must be enabled if we want to recreate XFS V5 filesystem
    if (xfstoolsver >= PROGVER(3,2,0)) // only use "crc" option when it is supported by mkfs
    {
        optval = (xfsver==XFS_SB_VERSION_5);
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m crc=%d ", (int)optval);
    }

    // Determine if the "finobt" mkfs option should be enabled (free inode btree)
    // - starting with linux-3.16 XFS has added a btree that tracks free inodes
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature is enabled by default when using xfsprogs-3.2.3 or later
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(3,2,1)) // only use "finobt" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_FINOBT));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m finobt=%d ", (int)optval);
    }

    // Determine if the "rmapbt" mkfs option should be enabled (reverse mapping btree)
    // - starting with linux-4.8 XFS has added a btree that maps filesystem blocks to their owner
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(4,8,0)) // only use "rmapbt" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_RMAPBT));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m rmapbt=%d ", (int)optval);
    }

    // Determine if the "reflink" mkfs option should be enabled
    // - starting with linux-4.9 XFS has added support for reflinked files
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature is enabled by default when using xfsprogs-5.1.0 or later
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(4,9,0)) // only use "reflink" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_REFLINK));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m reflink=%d ", (int)optval);
    }

    // Determine if the "inobtcount" mkfs option should be enabled (inobt block counts)
    // - starting with linux-5.10 XFS has added block usage counters for inode btrees
    //   to reduce mount times
    // - this optional feature relies on the V5 on-disk format and on the "finobt"
    //   (free inode btree) mkfs option being enabled
    // - this feature is enabled by default when using xfsprogs-5.15.0 or later
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(5,10,0)) // only use "inobtcount" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_ro_compat & XFS_SB_FEAT_RO_COMPAT_INOBTCNT));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m inobtcount=%d ", (int)optval);
    }

    // Determine if the "bigtime" mkfs option should be enabled (large timestamps)
    // - starting with linux-5.10 XFS has added support for larger inode timestamps, beyond january 2038
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature is enabled by default when using xfsprogs-5.15.0 or later
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(5,10,0)) // only use "bigtime" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_incompat & XFS_SB_FEAT_INCOMPAT_BIGTIME));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m bigtime=%d ", (int)optval);
    }

    // Attempt to preserve filesystem UUID
    // - mkfs.xfs' "-m uuid=" option was added in version 4.3.0 and is the best way to set UUID
    // - XFS V4 UUID can be successfully set using either xfs_admin or mkfs.xfs >= 4.3.0
    // - XFS V5 UUID, on the other hand, can't be set by xfs_admin without enabling a superblock
    //   incompat flag which bumps minimal kernel version to linux-4.3 and sets two different UUIDs
    //   in the filesystem: user-visible and metadata. For this reason XFS version is forced to V4
    //   if xfsprogs < 4.3.0
    if (strlen(mkfsuuid) > 0)
        snprintf(uuid, sizeof(uuid), "%s", mkfsuuid);
    else if (dico_get_string(d, 0, FSYSHEADKEY_FSUUID, buffer, sizeof(buffer))==0)
        snprintf(uuid, sizeof(uuid), "%s", buffer);
    if (strlen(uuid)==36)
    {
        if (xfstoolsver >= PROGVER(4,3,0))
            strlcatf(mkfsopts, sizeof(mkfsopts), " -m uuid=%s ", uuid);
        else
            strlcatf(xadmopts, sizeof(xadmopts), " -U %s ", uuid);
    }

    // Determine if the "ftype" mkfs option should be enabled (filetype in dirent)
    // - this feature allows the inode type to be stored in the directory structure
    // - this feature is madatory on XFS V5 (always enabled) but it is optional on XFS V4
    // - mkfs.xfs 4.2.0 enabled it by default on XFS V4 filesystems
    // - the "ftype" option must be specified after the "crc" option in mkfs.xfs < 4.2.0:
    //   https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/commit/?id=b990de8ba4e2df2bc76a140799d3ddb4a0eac4ce
    // - do not set ftype=1 with crc=1 as mkfs.xfs may fail (at least with xfsprogs-3.2.2)
    // - XFS V4 with ftype=1 is supported since linux-3.13. We purposely always disable it
    //   on V4 filesystems to keep them compatible with older kernels
    if (xfstoolsver >= PROGVER(3,2,0)) // only use "ftype" option when it is supported by mkfs
    {
        // ftype is already set to 1 when it is XFS V5
        if (xfsver==XFS_SB_VERSION_4)
            strlcatf(mkfsopts, sizeof(mkfsopts), " -n ftype=0 ");
    }

    // Determine if the "sparse" mkfs option should be enabled (sparse inode allocation)
    // - starting with linux-4.2 XFS can allocate discontinuous inode chunks
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature is enabled by default when using xfsprogs-4.16.0 or later
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    // - this feature is supported since mkfs.xfs 4.2.0
    // - mkfs.xfs 4.5.0 in RHEL 7.3 carries a custom patch removing the option
    if (xfstoolsver >= PROGVER(4,2,0) &&
        !match_uname_r(RHEL7_KERNEL_ERE)) // only use "sparse" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_incompat & XFS_SB_FEAT_INCOMPAT_SPINODES));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -i sparse=%d ", (int)optval);
    }

    // Determine if the "nrext64" mkfs option should be enabled (large extent counters)
    // - starting with linux-5.19 XFS has added support for larger number of per-file extents
    // - this feature relies on the V5 on-disk format but it is optional
    // - this feature will be enabled if the original filesystem was XFS V5 and had it
    if (xfstoolsver >= PROGVER(5,19,0)) // only use "nrext64" option when it is supported by mkfs
    {
        optval = ((xfsver==XFS_SB_VERSION_5) && (sb_features_incompat & XFS_SB_FEAT_INCOMPAT_NREXT64));
        strlcatf(mkfsopts, sizeof(mkfsopts), " -i nrext64=%d ", (int)optval);
    }

    // Determine if the "exchange range" option should be enabled
    if (xfstoolsver >= PROGVER(6,10,0))
    {
        optval = !!(sb_features_incompat & XFS_SB_FEAT_INCOMPAT_EXCHRANGE);
        strlcatf(mkfsopts, sizeof(mkfsopts), " -i exchange=%d ", (int)optval);
    }

    // Determine if the "directory parent pointers" option should be enabled
    if (xfstoolsver >= PROGVER(6,10,0))
    {
        optval = !!(sb_features_incompat & XFS_SB_FEAT_INCOMPAT_PARENT);
        strlcatf(mkfsopts, sizeof(mkfsopts), " -n parent=%d ", (int)optval);
    }

    // Determine if the "metadata directory" option should be enabled
    if (xfstoolsver >= PROGVER(6,13,0))
    {
        optval = !!(sb_features_incompat & XFS_SB_FEAT_INCOMPAT_METADIR);
        strlcatf(mkfsopts, sizeof(mkfsopts), " -m metadir=%d ", (int)optval);
    }

    // Determine if the "zoned" option should be enabled
    if (xfstoolsver >= PROGVER(6,15,0))
    {
        optval = !!(sb_features_incompat & XFS_SB_FEAT_INCOMPAT_ZONED);
        strlcatf(mkfsopts, sizeof(mkfsopts), " -r zoned=%d ", (int)optval);
    }

    // ---- mkfsopt from command line
    strlcatf(mkfsopts, sizeof(mkfsopts), " %s ", fsoptions);

    // ---- create the new filesystem using mkfs.xfs
    if (exec_command(command, sizeof(command), &exitst, NULL, 0, NULL, 0, "mkfs.xfs -f %s %s", mkfsopts, partition)!=0 || exitst!=0)
    {   errprintf("command [%s] failed\n", command);
        return -1;
    }

    // ---- use xfs_admin to set the UUID if not already done with mkfs.xfs
    if (xadmopts[0])
    {
        if (exec_command(command, sizeof(command), &exitst, NULL, 0, NULL, 0, "xfs_admin %s %s", xadmopts, partition)!=0 || exitst!=-0)
        {   errprintf("command [%s] failed\n", command);
            return -1;
        }
    }

    return 0;
}

int xfs_getinfo(cdico *d, char *devname)
{
    struct xfs_sb sb;
    char uuid[512];
    u64 xfsver;
    u32 temp32;
    int ret=0;
    int fd;
    int res;

    u64 sb_features_compat=0;
    u64 sb_features_ro_compat=0;
    u64 sb_features_incompat=0;
    u64 sb_features_log_incompat=0;

    if ((fd=open64(devname, O_RDONLY|O_LARGEFILE))<0)
    {   ret=-1;
        goto xfs_read_sb_return;
    }

    res=read(fd, &sb, sizeof(sb));
    if (res!=sizeof(sb))
    {   ret=-1;
        goto xfs_read_sb_close;
    }

    // ---- check it's an XFS file system
    if (be32_to_cpu(sb.sb_magicnum) != XFS_SB_MAGIC)
    {   ret=-1;
        msgprintf(3, "sb.sb_magicnum!=XFS_SB_MAGIC\n");
        goto xfs_read_sb_close;
    }

    // ---- check XFS filesystem version
    xfsver=be16_to_cpu(sb.sb_versionnum) & XFS_SB_VERSION_NUMBITS;
    switch (xfsver)
    {
        case XFS_SB_VERSION_4:
        case XFS_SB_VERSION_5:
            msgprintf(MSG_DEBUG1, "Detected XFS filesystem version %d\n", (int)xfsver);
            dico_add_u64(d, 0, FSYSHEADKEY_FSXFSVERSION, xfsver);
            break;
        default:
            ret=-1;
            msgprintf(MSG_STACK, "Invalid XFS filesystem version: version=[%d]\n", (int)xfsver);
            goto xfs_read_sb_close;
            break;
    }

    // ---- label
    msgprintf(MSG_DEBUG1, "xfs_label=[%s]\n", sb.sb_fname);
    dico_add_string(d, 0, FSYSHEADKEY_FSLABEL, (char*)sb.sb_fname);

    // ---- uuid
    /*if ((str=e2p_uuid2str(&sb.sb_uuid))!=NULL)
        dico_add_string(d, 0, FSYSHEADKEY_FSUUID, str);*/
    memset(uuid, 0, sizeof(uuid));
    uuid_unparse_lower((u8*)&sb.sb_uuid, uuid);
    dico_add_string(d, 0, FSYSHEADKEY_FSUUID, uuid);
    msgprintf(MSG_DEBUG1, "xfs_uuid=[%s]\n", uuid);

    // ---- block size
    temp32=be32_to_cpu(sb.sb_blocksize);
    if ((temp32%512!=0) || (temp32<512) || (temp32>65536))
    {   ret=-1;
        msgprintf(3, "xfs_blksize=[%ld] is an invalid xfs block size\n", (long)temp32);
        goto xfs_read_sb_close;
    }
    dico_add_u64(d, 0, FSYSHEADKEY_FSXFSBLOCKSIZE, temp32);
    msgprintf(MSG_DEBUG1, "xfs_blksize=[%ld]\n", (long)temp32);

    // ---- get filesystem features (will all be set to 0 if this is XFS V4)
    if (xfsver == XFS_SB_VERSION_5)
    {
        sb_features_compat=be32_to_cpu(sb.sb_features_compat);
        sb_features_ro_compat=be32_to_cpu(sb.sb_features_ro_compat);
        sb_features_incompat=be32_to_cpu(sb.sb_features_incompat);
        sb_features_log_incompat=be32_to_cpu(sb.sb_features_log_incompat);
    }

    // ---- check fsarchiver is aware of all the filesystem features used on that filesystem
    if (xfs_check_compatibility(sb_features_compat, sb_features_ro_compat, sb_features_incompat, sb_features_log_incompat)!=0)
        return -1;

    // ---- store features in the archive metadata
    dico_add_u64(d, 0, FSYSHEADKEY_FSXFSFEATURECOMPAT, (u64)sb_features_compat);
    dico_add_u64(d, 0, FSYSHEADKEY_FSXFSFEATUREROCOMPAT, (u64)sb_features_ro_compat);
    dico_add_u64(d, 0, FSYSHEADKEY_FSXFSFEATUREINCOMPAT, (u64)sb_features_incompat);
    dico_add_u64(d, 0, FSYSHEADKEY_FSXFSFEATURELOGINCOMPAT, (u64)sb_features_log_incompat);

    // ---- minimum fsarchiver version required to restore
    dico_add_u64(d, 0, FSYSHEADKEY_MINFSAVERSION, FSA_VERSION_BUILD(0, 6, 20, 0));

xfs_read_sb_close:
    close(fd);
xfs_read_sb_return:
    return ret;
}

int xfs_mount(char *partition, char *mntbuf, char *fsbuf, int flags, char *mntinfo)
{
    return generic_mount(partition, mntbuf, fsbuf, "nouuid", flags);
}

int xfs_umount(char *partition, char *mntbuf)
{
    return generic_umount(mntbuf);
}

int xfs_test(char *devname)
{
    struct xfs_sb sb;
    int fd;

    if ((fd=open64(devname, O_RDONLY|O_LARGEFILE))<0)
    {
        msgprintf(MSG_DEBUG1, "open64(%s) failed\n", devname);
        return false;
    }

    memset(&sb, 0, sizeof(sb));
    if (read(fd, &sb, sizeof(sb))!=sizeof(sb))
    {   close(fd);
        msgprintf(MSG_DEBUG1, "read failed\n");
        return false;
    }

    // ---- check it's an XFS file system
    if (be32_to_cpu(sb.sb_magicnum) != XFS_SB_MAGIC)
    {   close(fd);
        msgprintf(MSG_DEBUG1, "(be32_to_cpu(sb.sb_magicnum)=%.8x) != (XFS_SB_MAGIC=%.8x)\n", be32_to_cpu(sb.sb_magicnum), XFS_SB_MAGIC);
        return false;
    }

    close(fd);
    return true;
}

int xfs_get_reqmntopt(char *partition, cstrlist *reqopt, cstrlist *badopt)
{
    if (!reqopt || !badopt)
        return -1;

    return 0;
}
