/* vi: set expandtab sw=4 sts=4: */
/* opkg_remove.c - the opkg package management system

   Carl D. Worth

   Copyright (C) 2001 University of Southern California

   SPDX-License-Identifier: GPL-2.0-or-later

   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 2, 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.
*/

#include "config.h"

#include <libgen.h>
#include <stdio.h>
#include <glob.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>

#include "opkg_message.h"
#include "opkg_remove.h"
#include "opkg_cmd.h"
#include "file_util.h"
#include "sprintf_alloc.h"
#include "xfuncs.h"

void remove_data_files_and_list(pkg_t * pkg)
{
    str_list_t installed_dirs, installed_dirs_symlinks;
    str_list_elt_t *iter;
    file_list_t *installed_files;
    file_list_elt_t *fiter;
    char *file_name;
    conffile_t *conffile;
    int removed_a_dir, removed_a_dir_symlink;
    pkg_t *owner;
    int rootdirlen = 0;
    int r;

    installed_files = pkg_get_installed_files(pkg);
    if (installed_files == NULL) {
        opkg_msg(ERROR,
                 "Failed to determine installed "
                 "files for %s. None removed.\n", pkg->name);
        return;
    }

    str_list_init(&installed_dirs);
    str_list_init(&installed_dirs_symlinks);

    /* don't include trailing slash */
    if (opkg_config->offline_root)
        rootdirlen = strlen(opkg_config->offline_root);

    for (fiter = file_list_first(installed_files); fiter;
            fiter = file_list_next(installed_files, fiter)) {
        file_info_t *file_info = (file_info_t *)fiter->data;
        file_name = file_info->path;

        owner = file_hash_get_file_owner(file_name);
        if (owner != pkg)
            /* File may have been claimed by another package. */
            continue;

        if (file_is_dir(file_name)) {
            /*
               Ensure that the directory is marked for deletion only if
               no other installed package is using that directory.
             */
            if (dir_hash_get_ref_count(file_name) == 1)
            {
                str_list_append(&installed_dirs, file_name);
                dir_hash_remove(file_name);
            }
            else
            {
                /* directory should get a new owner in the file hash*/
                file_hash_remove(file_name);
            }
            continue;
        } else if (file_is_symlink(file_name)) {
            char *link_target;
            struct stat target_stat;

            link_target = realpath(file_name, NULL);
            if (link_target) {
                if ((xlstat(link_target, &target_stat) == 0) && S_ISDIR(target_stat.st_mode)) {
                    /*
                     * Ensure that the symlink is marked for deletion
                     * only if no other installed package uses that symlink.
                     */
                    if (dir_hash_get_ref_count(file_name) == 1)
                    {
                        str_list_append(&installed_dirs_symlinks, file_name);
                        dir_hash_remove(file_name);
                    }
                    else
                    {
                        /* directory should get a new owner in the file hash*/
                        file_hash_remove(file_name);
                    }

                    free(link_target);
                    continue;
                }
                free(link_target);
            }
        }

        conffile = pkg_get_conffile(pkg, file_name + rootdirlen);
        if (conffile) {
            if (conffile_has_been_modified(conffile)) {
                opkg_msg(NOTICE, "Not deleting modified conffile %s.\n",
                         file_name);
                continue;
            }
        }

        if (!opkg_config->noaction) {
            opkg_msg(INFO, "Deleting %s.\n", file_name);
            unlink(file_name);
        } else
            opkg_msg(INFO, "Not deleting %s. (noaction)\n", file_name);

        file_hash_remove(file_name);
    }

    /* Remove empty directories */
    if (!opkg_config->noaction) {
        do {
            removed_a_dir = 0;
            for (iter = str_list_first(&installed_dirs); iter;
                    iter = str_list_next(&installed_dirs, iter)) {
                file_name = (char *)iter->data;

                r = rmdir(file_name);
                if (r == 0) {
                    opkg_msg(INFO, "Deleting %s.\n", file_name);
                    removed_a_dir = 1;
                    str_list_remove(&installed_dirs, &iter);
                }
            }
        } while (removed_a_dir);
        do {
            removed_a_dir_symlink = 0;
            for (iter = str_list_first(&installed_dirs_symlinks); iter;
                    iter = str_list_next(&installed_dirs_symlinks, iter)) {
                file_name = (char *)iter->data;

                r = unlink(file_name);
                if (r == 0) {
                    opkg_msg(INFO, "Deleting %s.\n", file_name);
                    removed_a_dir_symlink = 1;
                    str_list_remove(&installed_dirs_symlinks, &iter);
                }
            }
        } while (removed_a_dir_symlink);
    }

    pkg_free_installed_files(pkg);
    pkg_remove_installed_files_list(pkg);

    /* Don't print warning for dirs that are provided by other packages */
    for (iter = str_list_first(&installed_dirs); iter;
            iter = str_list_next(&installed_dirs, iter)) {
        file_name = (char *)iter->data;

        owner = file_hash_get_file_owner(file_name);
        if (owner) {
            free(iter->data);
            iter->data = NULL;
            str_list_remove(&installed_dirs, &iter);
        }
    }

    /* cleanup */
    while (!void_list_empty(&installed_dirs)) {
        iter = str_list_pop(&installed_dirs);
        free(iter->data);
        free(iter);
    }
    while (!void_list_empty(&installed_dirs_symlinks)) {
        iter = str_list_pop(&installed_dirs_symlinks);
        free(iter->data);
        free(iter);
    }
    str_list_deinit(&installed_dirs);
    str_list_deinit(&installed_dirs_symlinks);
}

void remove_maintainer_scripts(pkg_t * pkg)
{
    unsigned int i;
    int err;
    char *globpattern, *filename, *lastdot;
    glob_t globbuf;

    if (opkg_config->noaction)
        return;

    sprintf_alloc(&globpattern, "%s/%s.*", pkg->dest->info_dir, pkg->name);

    err = glob(globpattern, 0, NULL, &globbuf);
    free(globpattern);
    if (err)
        return;

    for (i = 0; i < globbuf.gl_pathc; i++) {
        filename = xstrdup(basename(globbuf.gl_pathv[i]));
        lastdot = strrchr(filename, '.');
        *lastdot = '\0';
        // Only delete files that match the package name (the glob may match files
        // with similar names)
        if (!strcmp(filename, pkg->name)) {
            opkg_msg(INFO, "Deleting %s.\n", globbuf.gl_pathv[i]);
            unlink(globbuf.gl_pathv[i]);
        }
        free(filename);
    }
    globfree(&globbuf);
}

int opkg_remove_pkg(pkg_t * pkg)
{
    int err;
    int r;

    if (opkg_config->download_only)
	return 0;
    /*
     * If called from an upgrade and not from a normal remove,
     * ignore the essential flag.
     */
    if (pkg->essential) {
        if (opkg_config->force_removal_of_essential_packages) {
            opkg_msg(NOTICE,
                     "Removing essential package %s under your coercion.\n"
                     "\tIf your system breaks, you get to keep both pieces\n",
                     pkg->name);
        } else {
            opkg_msg(NOTICE,
                     "Refusing to remove essential package %s.\n"
                     "\tRemoving an essential package may lead to an unusable system, but if\n"
                     "\tyou enjoy that kind of pain, you can force opkg to proceed against\n"
                     "\tits will with the option: --force-removal-of-essential-packages\n",
                     pkg->name);
            return -1;
        }
    }

    if (pkg->parent == NULL)
        return 0;

    opkg_msg(NOTICE, "Removing %s (%s) from %s...\n", pkg->name,
             pkg->version, pkg->dest->name);
    pkg->state_flag |= SF_FILELIST_CHANGED;

    pkg->state_want = SW_DEINSTALL;
    opkg_state_changed++;

    r = pkg_run_script(pkg, "prerm", "remove");
    if (r != 0) {
        if (!opkg_config->force_remove) {
            opkg_msg(ERROR,
                     "not removing package \"%s\", " "prerm script failed\n",
                     pkg->name);
            opkg_msg(NOTICE,
                     "You can force removal of packages with failed "
                     "prerm scripts with the option: \n" "\t--force-remove\n");
            return -1;
        }
    }

    /* DPKG_INCOMPATIBILITY: dpkg is slightly different here. It
     * maintains an empty filelist rather than deleting it. That seems
     * like a big pain, and I don't see that that should make a big
     * difference, but for anyone who wants tighter compatibility,
     * feel free to fix this. */
    remove_data_files_and_list(pkg);

    err = pkg_run_script(pkg, "postrm", "remove");

    remove_maintainer_scripts(pkg);
    pkg->state_status = SS_NOT_INSTALLED;

    pkg->parent->state_status = SS_NOT_INSTALLED;

    return err;
}
