/*
  Copyright 2024 Northern.tech AS

  This file is part of CFEngine 3 - written and maintained by Northern.tech AS.

  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; version 3.

  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, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA

  To the extent this program is licensed as part of the Enterprise
  versions of CFEngine, the applicable Commercial Open Source License
  (COSL) may apply to this file if you as a licensee so wish it. See
  included file COSL.txt.
*/

#include <files_select.h>

#include <eval_context.h>
#include <files_names.h>
#include <files_interfaces.h>
#include <promises.h>
#include <match_scope.h>
#include <string_lib.h>
#include <files_lib.h>
#include <pipes.h>
#include <promises.h>
#include <exec_tools.h>
#include <chflags.h>
#include <unix.h>               /* GetGroupName(), GetUserName() */

static bool SelectTypeMatch(const struct stat *lstatptr, Rlist *crit);
static bool SelectOwnerMatch(EvalContext *ctx, char *path, const struct stat *lstatptr, Rlist *crit);
static bool SelectModeMatch(const struct stat *lstatptr, Rlist *ls);
static bool SelectTimeMatch(time_t stattime, time_t fromtime, time_t totime);
static bool SelectNameRegexMatch(EvalContext *ctx, const char *filename, char *crit);
static bool SelectPathRegexMatch(EvalContext *ctx, char *filename, char *crit);
static bool SelectExecRegexMatch(EvalContext *ctx, char *filename, char *crit, char *prog);
static bool SelectIsSymLinkTo(EvalContext *ctx, char *filename, Rlist *crit);
static bool SelectExecProgram(char *filename, char *command);
static bool SelectSizeMatch(size_t size, size_t min, size_t max);

#if !defined(__MINGW32__)
static bool SelectGroupMatch(EvalContext *ctx, const struct stat *lstatptr, Rlist *crit);
#endif

#if defined HAVE_CHFLAGS
static bool SelectBSDMatch(const struct stat *lstatptr, Rlist *bsdflags);
#endif

bool SelectLeaf(EvalContext *ctx, char *path, const struct stat *sb, const FileSelect *fs)
{
    Rlist *rp;

    StringSet *leaf_attr = StringSetNew();

#ifdef __MINGW32__
    if (fs->issymlinkto != NULL)
    {
        Log(LOG_LEVEL_VERBOSE,
              "files_select.issymlinkto is ignored on Windows (symbolic links are not supported by Windows)");
    }

    if (fs->groups != NULL)
    {
        Log(LOG_LEVEL_VERBOSE,
              "files_select.search_groups is ignored on Windows (file groups are not supported by Windows)");
    }

    if (fs->bsdflags != NULL)
    {
        Log(LOG_LEVEL_VERBOSE, "files_select.search_bsdflags is ignored on Windows");
    }
#endif /* __MINGW32__ */

    if (fs->name == NULL)
    {
        StringSetAdd(leaf_attr, xstrdup("leaf_name"));
    }

    for (rp = fs->name; rp != NULL; rp = rp->next)
    {
        if (SelectNameRegexMatch(ctx, path, RlistScalarValue(rp)))
        {
            StringSetAdd(leaf_attr, xstrdup("leaf_name"));
            break;
        }
    }

    if (fs->path == NULL)
    {
        StringSetAdd(leaf_attr, xstrdup("leaf_path"));
    }

    for (rp = fs->path; rp != NULL; rp = rp->next)
    {
        if (SelectPathRegexMatch(ctx, path, RlistScalarValue(rp)))
        {
            StringSetAdd(leaf_attr, xstrdup("path_name"));
            break;
        }
    }

    if (SelectTypeMatch(sb, fs->filetypes))
    {
        StringSetAdd(leaf_attr, xstrdup("file_types"));
    }

    if ((fs->owners) && (SelectOwnerMatch(ctx, path, sb, fs->owners)))
    {
        StringSetAdd(leaf_attr, xstrdup("owner"));
    }

    if (fs->owners == NULL)
    {
        StringSetAdd(leaf_attr, xstrdup("owner"));
    }

#ifdef __MINGW32__
    StringSetAdd(leaf_attr, xstrdup("group"));

#else /* !__MINGW32__ */
    if ((fs->groups) && (SelectGroupMatch(ctx, sb, fs->groups)))
    {
        StringSetAdd(leaf_attr, xstrdup("group"));
    }

    if (fs->groups == NULL)
    {
        StringSetAdd(leaf_attr, xstrdup("group"));
    }
#endif /* !__MINGW32__ */

    if (SelectModeMatch(sb, fs->perms))
    {
        StringSetAdd(leaf_attr, xstrdup("mode"));
    }

#if defined HAVE_CHFLAGS
    if (SelectBSDMatch(sb, fs->bsdflags))
    {
        StringSetAdd(leaf_attr, xstrdup("bsdflags"));
    }
#endif

    if (SelectTimeMatch(sb->st_atime, fs->min_atime, fs->max_atime))
    {
        StringSetAdd(leaf_attr, xstrdup("atime"));
    }

    if (SelectTimeMatch(sb->st_ctime, fs->min_ctime, fs->max_ctime))
    {
        StringSetAdd(leaf_attr, xstrdup("ctime"));
    }

    if (SelectSizeMatch(sb->st_size, fs->min_size, fs->max_size))
    {
        StringSetAdd(leaf_attr, xstrdup("size"));
    }

    if (SelectTimeMatch(sb->st_mtime, fs->min_mtime, fs->max_mtime))
    {
        StringSetAdd(leaf_attr, xstrdup("mtime"));
    }

    if ((fs->issymlinkto) && (SelectIsSymLinkTo(ctx, path, fs->issymlinkto)))
    {
        StringSetAdd(leaf_attr, xstrdup("issymlinkto"));
    }

    if ((fs->exec_regex) && (SelectExecRegexMatch(ctx, path, fs->exec_regex, fs->exec_program)))
    {
        StringSetAdd(leaf_attr, xstrdup("exec_regex"));
    }

    if ((fs->exec_program) && (SelectExecProgram(path, fs->exec_program)))
    {
        StringSetAdd(leaf_attr, xstrdup("exec_program"));
    }

    bool result = EvalFileResult(fs->result, leaf_attr);

    Log(LOG_LEVEL_VERBOSE, "file_select result '%s' on '%s' was '%s'",
        fs->result, path,
        result ? "true" : "false");

    StringSetDestroy(leaf_attr);

    return result;
}

/*******************************************************************/
/* Level                                                           */
/*******************************************************************/

static bool SelectSizeMatch(size_t size, size_t min, size_t max)
{
    if ((size <= max) && (size >= min))
    {
        return true;
    }

    return false;
}

/*******************************************************************/

static bool SelectTypeMatch(const struct stat *lstatptr, Rlist *crit)
{
    Rlist *rp;

    StringSet *leafattrib = StringSetNew();

    if (S_ISREG(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("reg"));
        StringSetAdd(leafattrib, xstrdup("plain"));
    }

    if (S_ISDIR(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("dir"));
    }

#ifndef __MINGW32__
    if (S_ISLNK(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("symlink"));
    }

    if (S_ISFIFO(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("fifo"));
    }

    if (S_ISSOCK(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("socket"));
    }

    if (S_ISCHR(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("char"));
    }

    if (S_ISBLK(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("block"));
    }
#endif /* !__MINGW32__ */

#ifdef HAVE_DOOR_CREATE
    if (S_ISDOOR(lstatptr->st_mode))
    {
        StringSetAdd(leafattrib, xstrdup("door"));
    }
#endif

    for (rp = crit; rp != NULL; rp = rp->next)
    {
        if (EvalFileResult(RlistScalarValue(rp), leafattrib))
        {
            StringSetDestroy(leafattrib);
            return true;
        }
    }

    StringSetDestroy(leafattrib);
    return false;
}

static bool SelectOwnerMatch(EvalContext *ctx, char *path, const struct stat *lstatptr, Rlist *crit)
{
    Rlist *rp;
    char ownerName[CF_BUFSIZE];

    StringSet *leafattrib = StringSetNew();

#ifndef __MINGW32__                   // no uids on Windows
    char buffer[CF_SMALLBUF];
    snprintf(buffer, CF_SMALLBUF, "%ju", (uintmax_t) lstatptr->st_uid);
    StringSetAdd(leafattrib, xstrdup(buffer));
#endif /* __MINGW32__ */

    bool gotOwner = GetOwnerName(path, lstatptr, ownerName, sizeof(ownerName));

    if (gotOwner)
    {
        StringSetAdd(leafattrib, xstrdup(ownerName));
    }
    else
    {
        StringSetAdd(leafattrib, xstrdup("none"));
    }

    for (rp = crit; rp != NULL; rp = rp->next)
    {
        if (EvalFileResult(RlistScalarValue(rp), leafattrib))
        {
            Log(LOG_LEVEL_DEBUG, "Select owner match");
            StringSetDestroy(leafattrib);
            return true;
        }

        if (gotOwner && (FullTextMatch(ctx, RlistScalarValue(rp), ownerName)))
        {
            Log(LOG_LEVEL_DEBUG, "Select owner match");
            StringSetDestroy(leafattrib);
            return true;
        }

#ifndef __MINGW32__
        if (FullTextMatch(ctx, RlistScalarValue(rp), buffer))
        {
            Log(LOG_LEVEL_DEBUG, "Select owner match");
            StringSetDestroy(leafattrib);
            return true;
        }
#endif /* !__MINGW32__ */
    }

    StringSetDestroy(leafattrib);
    return false;
}

/*******************************************************************/

static bool SelectModeMatch(const struct stat *lstatptr, Rlist *list)
{
    mode_t newperm, plus, minus;
    Rlist *rp;

    for (rp = list; rp != NULL; rp = rp->next)
    {
        plus = 0;
        minus = 0;

        if (!ParseModeString(RlistScalarValue(rp), &plus, &minus))
        {
            Log(LOG_LEVEL_ERR, "Problem validating a mode string '%s' in search filter", RlistScalarValue(rp));
            continue;
        }

        newperm = (lstatptr->st_mode & 07777);
        newperm |= plus;
        newperm &= ~minus;

        if ((newperm & 07777) == (lstatptr->st_mode & 07777))
        {
            return true;
        }
    }

    return false;
}

/*******************************************************************/

#if defined HAVE_CHFLAGS
static bool SelectBSDMatch(const struct stat *lstatptr, Rlist *bsdflags)
{
    u_long newflags, plus, minus;

    if (!ParseFlagString(bsdflags, &plus, &minus))
    {
        Log(LOG_LEVEL_ERR, "Problem validating a BSD flag string");
    }

    newflags = (lstatptr->st_flags & CHFLAGS_MASK);
    newflags |= plus;
    newflags &= ~minus;

    if ((newflags & CHFLAGS_MASK) == (lstatptr->st_flags & CHFLAGS_MASK))       /* file okay */
    {
        return true;
    }

    return false;
}
#endif
/*******************************************************************/

static bool SelectTimeMatch(time_t stattime, time_t fromtime, time_t totime)
{
    return ((fromtime < stattime) && (stattime < totime));
}

/*******************************************************************/

static bool SelectNameRegexMatch(EvalContext *ctx, const char *filename, char *crit)
{
    if (FullTextMatch(ctx, crit, ReadLastNode(filename)))
    {
        return true;
    }

    return false;
}

/*******************************************************************/

static bool SelectPathRegexMatch(EvalContext *ctx, char *filename, char *crit)
{
    if (FullTextMatch(ctx, crit, filename))
    {
        return true;
    }

    return false;
}

/*******************************************************************/

static bool SelectExecRegexMatch(EvalContext *ctx, char *filename, char *crit, char *prog)
{
    // insert real value of $(this.promiser) in command

    char *buf_tmp = SearchAndReplace(prog, "$(this.promiser)", filename);
    char *buf = SearchAndReplace(buf_tmp, "${this.promiser}", filename);
    free(buf_tmp);

    FILE *pp = cf_popen(buf, "r", true);
    if (pp == NULL)
    {
        Log(LOG_LEVEL_ERR, "Couldn't open pipe to command '%s'. (cf_popen: %s)", buf, GetErrorStr());
        free(buf);
        return false;
    }

    size_t line_size = CF_BUFSIZE;
    char *line = xmalloc(line_size);

    for (;;)
    {
        ssize_t res = CfReadLine(&line, &line_size, pp);
        if (res == -1)
        {
            if (!feof(pp))
            {
                Log(LOG_LEVEL_ERR, "Error reading output from command '%s'. (fgets: %s)", buf, GetErrorStr());
            }
            cf_pclose(pp);
            free(line);
            free(buf);
            return false;
        }

        if (FullTextMatch(ctx, crit, line))
        {
            cf_pclose(pp);
            free(line);
            free(buf);
            return true;
        }
    }

    cf_pclose(pp);
    free(line);
    free(buf);
    return false;
}

/*******************************************************************/

static bool SelectIsSymLinkTo(EvalContext *ctx, char *filename, Rlist *crit)
{
#ifndef __MINGW32__
    char buffer[CF_BUFSIZE];
    Rlist *rp;

    for (rp = crit; rp != NULL; rp = rp->next)
    {
        memset(buffer, 0, CF_BUFSIZE);

        struct stat statbuf;

        // Don't worry if this gives an error, that's handled above us.

        // We're calling lstat() here to avoid dereferencing the
        // symlink... and we only care if the inode is a directory.
        if (lstat(filename, &statbuf) == -1)
        {
            // Do nothing.
        }
        else if (!S_ISLNK(statbuf.st_mode))
        {
            Log(LOG_LEVEL_DEBUG, "Skipping readlink() on non-symlink %s", filename);
            return false;
        }

        if (readlink(filename, buffer, CF_BUFSIZE - 1) == -1)
        {
            Log(LOG_LEVEL_ERR, "Unable to read link '%s' in filter. (readlink: %s)",
                filename, GetErrorStr());
            return false;
        }

        if (FullTextMatch(ctx, RlistScalarValue(rp), buffer))
        {
            return true;
        }
    }
#endif /* !__MINGW32__ */
    return false;
}

/*******************************************************************/

static bool SelectExecProgram(char *filename, char *command)
  /* command can include $(this.promiser) for the name of the file */
{
// insert real value of $(this.promiser) in command

    char *buf_tmp = SearchAndReplace(command, "$(this.promiser)", filename);
    char *buf = SearchAndReplace(buf_tmp, "${this.promiser}", filename);
    free(buf_tmp);

    bool returns_zero = ShellCommandReturnsZero(buf, SHELL_TYPE_NONE);

    if (returns_zero)
    {
        Log(LOG_LEVEL_DEBUG, "Select ExecProgram match for '%s'", buf);
        free(buf);
        return true;
    }
    else
    {
        free(buf);
        return false;
    }
}

#ifndef __MINGW32__

/*******************************************************************/
/* Unix implementations                                            */
/*******************************************************************/

bool GetOwnerName(char *path, const struct stat *lstatptr, char *owner, int ownerSz)
{
    memset(owner, 0, ownerSz);
    if (!GetUserName(lstatptr->st_uid, owner, ownerSz, LOG_LEVEL_ERR))
    {
        /* detailed error already logged */
        Log(LOG_LEVEL_ERR, "Could not get owner name for '%s'", path);
        return false;
    }

    return true;
}

/*******************************************************************/

static bool SelectGroupMatch(EvalContext *ctx, const struct stat *lstatptr, Rlist *crit)
{
    char buffer[CF_SMALLBUF];
    Rlist *rp;

    StringSet *leafattrib = StringSetNew();

    snprintf(buffer, CF_SMALLBUF, "%ju", (uintmax_t) lstatptr->st_gid);
    StringSetAdd(leafattrib, xstrdup(buffer));

    bool found = GetGroupName(lstatptr->st_gid, buffer, sizeof(buffer), LOG_LEVEL_VERBOSE);
    if (found)
    {
        StringSetAdd(leafattrib, xstrdup(buffer));
    }
    else
    {
        StringSetAdd(leafattrib, xstrdup("none"));
    }

    for (rp = crit; rp != NULL; rp = rp->next)
    {
        if (EvalFileResult(RlistScalarValue(rp), leafattrib))
        {
            Log(LOG_LEVEL_DEBUG, "Select group match");
            StringSetDestroy(leafattrib);
            return true;
        }

        if (found && (FullTextMatch(ctx, RlistScalarValue(rp), buffer)))
        {
            Log(LOG_LEVEL_DEBUG, "Select group match");
            StringSetDestroy(leafattrib);
            return true;
        }

        if (FullTextMatch(ctx, RlistScalarValue(rp), buffer))
        {
            Log(LOG_LEVEL_DEBUG, "Select group match");
            StringSetDestroy(leafattrib);
            return true;
        }
    }

    StringSetDestroy(leafattrib);
    return false;
}

#endif /* !__MINGW32__ */
