root/usr.sbin/bsdinstall/distextract/distextract.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2011 Nathan Whitehorn
 * Copyright (c) 2014 Devin Teske <dteske@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/param.h>

#include <archive.h>
#include <ctype.h>
#include <bsddialog.h>
#include <bsddialog_progressview.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "opt_osname.h"

/* Data to process */
static const char *distdir = NULL;
static struct archive *archive = NULL;

/* Function prototypes */
static void     sig_int(int sig);
static int      count_files(const char *file);
static int      extract_files(struct bsddialog_fileminibar *file);

#define _errx(...) (bsddialog_end(), errx(__VA_ARGS__))

int
main(void)
{
        char *chrootdir;
        char *distributions;
        char *distribs, *distrib;
        int retval;
        size_t minibar_size = sizeof(struct bsddialog_fileminibar);
        unsigned int nminibars;
        struct bsddialog_fileminibar *dists;
        struct bsddialog_progviewconf pvconf;
        struct bsddialog_conf conf;
        struct sigaction act;
        char error[PATH_MAX + 512];

        if ((distributions = getenv("DISTRIBUTIONS")) == NULL)
                errx(EXIT_FAILURE, "DISTRIBUTIONS variable is not set");
        if ((distdir = getenv("BSDINSTALL_DISTDIR")) == NULL)
                distdir = "";
        if ((distribs = strdup(distributions)) == NULL)
                errx(EXIT_FAILURE, "memory error");

        if (bsddialog_init() == BSDDIALOG_ERROR)
                errx(EXIT_FAILURE, "Error libbsdialog: %s",
                    bsddialog_geterror());
        bsddialog_initconf(&conf);
        bsddialog_backtitle(&conf, OSNAME " Installer");
        bsddialog_infobox(&conf,
            "Checking distribution archives.\nPlease wait...", 4, 35);

        /* Parse $DISTRIBUTIONS */
        nminibars = 0;
        dists = NULL;
        while ((distrib = strsep(&distribs, "\t\n\v\f\r ")) != NULL) {
                if (strlen(distrib) == 0)
                        continue;

                /* Allocate a new struct for the distribution */
                dists = realloc(dists, (nminibars + 1) * minibar_size);
                if (dists == NULL)
                        _errx(EXIT_FAILURE, "Out of memory!");

                /* Set file path */
                dists[nminibars].path = distrib;

                /* Set mini bar label */
                dists[nminibars].label = strrchr(dists[nminibars].path, '/');
                if (dists[nminibars].label == NULL)
                        dists[nminibars].label = dists[nminibars].path;

                /* Set initial length in files (-1 == error) */
                dists[nminibars].size = count_files(dists[nminibars].path);
                if (dists[nminibars].size < 0) {
                        bsddialog_end();
                        return (EXIT_FAILURE);
                }

                /* Set initial status and implicitly miniperc to pending */
                dists[nminibars].status = BSDDIALOG_MG_PENDING;

                /* Set initial read */
                dists[nminibars].read = 0;

                nminibars += 1;
        }

        /* Optionally chdir(2) into $BSDINSTALL_CHROOT */
        chrootdir = getenv("BSDINSTALL_CHROOT");
        if (chrootdir != NULL && chdir(chrootdir) != 0) {
                snprintf(error, sizeof(error),
                    "Could not change to directory %s: %s\n",
                    chrootdir, strerror(errno));
                conf.title = "Error";
                bsddialog_msgbox(&conf, error, 0, 0);
                bsddialog_end();
                return (EXIT_FAILURE);
        }

        /* Set cleanup routine for Ctrl-C action */
        act.sa_handler = sig_int;
        sigaction(SIGINT, &act, 0);

        conf.title = "Archive Extraction";
        conf.auto_minwidth = 40;
        pvconf.callback = extract_files;
        pvconf.refresh = 1;
        pvconf.fmtbottomstr = "%10lli files read @ %'9.1f files/sec.";
        bsddialog_total_progview = 0;
        bsddialog_interruptprogview = bsddialog_abortprogview = false;
        retval = bsddialog_progressview(&conf,
            "\nExtracting distribution files...\n", 0, 0,
            &pvconf, nminibars, dists);

        if (retval == BSDDIALOG_ERROR) {
                fprintf(stderr, "progressview error: %s\n",
                    bsddialog_geterror());
        }

        bsddialog_end();

        free(distribs);
        free(dists);

        return (retval);
}

static void
sig_int(int sig __unused)
{
        bsddialog_interruptprogview = true;
}

/*
 * Returns number of files in archive file. Parses $BSDINSTALL_DISTDIR/MANIFEST
 * if it exists, otherwise uses archive(3) to read the archive file.
 */
static int
count_files(const char *file)
{
        static FILE *manifest = NULL;
        char *p;
        int file_count;
        int retval;
        size_t span;
        struct archive_entry *entry;
        char line[512];
        char path[PATH_MAX];
        char errormsg[PATH_MAX + 512];
        struct bsddialog_conf conf;

        if (manifest == NULL) {
                snprintf(path, sizeof(path), "%s/MANIFEST", distdir);
                manifest = fopen(path, "r");
        }

        if (manifest != NULL) {
                rewind(manifest);
                while (fgets(line, sizeof(line), manifest) != NULL) {
                        p = &line[0];
                        span = strcspn(p, "\t") ;
                        if (span < 1 || strncmp(p, file, span) != 0)
                                continue;

                        /*
                         * We're at the right manifest line. The file count is
                         * in the third element
                         */
                        span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
                        span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
                        if (span > 0) {
                                file_count = (int)strtol(p, (char **)NULL, 10);
                                if (file_count == 0 && errno == EINVAL)
                                        continue;
                                return (file_count);
                        }
                }
        }

        /*
         * Either no manifest, or manifest didn't mention this archive.
         * Use archive(3) to read the archive, counting files within.
         */
        bsddialog_initconf(&conf);
        if ((archive = archive_read_new()) == NULL) {
                snprintf(errormsg, sizeof(errormsg),
                    "Error: %s\n", archive_error_string(NULL));
                conf.title = "Extract Error";
                bsddialog_msgbox(&conf, errormsg, 0, 0);
                return (-1);
        }
        archive_read_support_format_all(archive);
        archive_read_support_filter_all(archive);
        snprintf(path, sizeof(path), "%s/%s", distdir, file);
        retval = archive_read_open_filename(archive, path, 4096);
        if (retval != ARCHIVE_OK) {
                snprintf(errormsg, sizeof(errormsg),
                    "Error while extracting %s: %s\n", file,
                    archive_error_string(archive));
                conf.title = "Extract Error";
                bsddialog_msgbox(&conf, errormsg, 0, 0);
                archive = NULL;
                return (-1);
        }

        file_count = 0;
        while (archive_read_next_header(archive, &entry) == ARCHIVE_OK)
                file_count++;
        archive_read_free(archive);
        archive = NULL;

        return (file_count);
}

static int
extract_files(struct bsddialog_fileminibar *file)
{
        int retval;
        struct archive_entry *entry;
        char path[PATH_MAX];
        char errormsg[PATH_MAX + 512];
        struct bsddialog_conf conf;

        bsddialog_initconf(&conf);

        /* Open the archive if necessary */
        if (archive == NULL) {
                if ((archive = archive_read_new()) == NULL) {
                        snprintf(errormsg, sizeof(errormsg),
                            "Error: %s\n", archive_error_string(NULL));
                        conf.title = "Extract Error";
                        bsddialog_msgbox(&conf, errormsg, 0, 0);
                        bsddialog_abortprogview = true;
                        return (-1);
                }
                archive_read_support_format_all(archive);
                archive_read_support_filter_all(archive);
                snprintf(path, sizeof(path), "%s/%s", distdir, file->path);
                retval = archive_read_open_filename(archive, path, 4096);
                if (retval != 0) {
                        snprintf(errormsg, sizeof(errormsg),
                            "Error opening %s: %s\n", file->label,
                            archive_error_string(archive));
                        conf.title = "Extract Error";
                        bsddialog_msgbox(&conf, errormsg, 0, 0);
                        file->status = BSDDIALOG_MG_FAILED;
                        bsddialog_abortprogview = true;
                        return (-1);
                }
        }

        /* Read the next archive header */
        retval = archive_read_next_header(archive, &entry);

        /* If that went well, perform the extraction */
        if (retval == ARCHIVE_OK)
                retval = archive_read_extract(archive, entry,
                    ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_OWNER |
                    ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL |
                    ARCHIVE_EXTRACT_XATTR | ARCHIVE_EXTRACT_FFLAGS);

        /* Test for either EOF or error */
        if (retval == ARCHIVE_EOF) {
                archive_read_free(archive);
                archive = NULL;
                file->status = BSDDIALOG_MG_DONE; /*Done*/;
                return (100);
        } else if (retval != ARCHIVE_OK &&
            !(retval == ARCHIVE_WARN &&
            strcmp(archive_error_string(archive), "Can't restore time") == 0)) {
                /*
                 * Print any warning/error messages except inability to set
                 * ctime/mtime, which is not fatal, or even interesting,
                 * for our purposes. Would be nice if this were a libarchive
                 * option.
                 */
                snprintf(errormsg, sizeof(errormsg),
                    "Error while extracting %s: %s\n", file->label,
                    archive_error_string(archive));
                conf.title = "Extract Error";
                bsddialog_msgbox(&conf, errormsg, 0, 0);
                file->status = BSDDIALOG_MG_FAILED; /* Failed */
                bsddialog_abortprogview = true;
                return (-1);
        }

        bsddialog_total_progview++;
        file->read++;

        /* Calculate [overall] percentage of completion (if possible) */
        if (file->size >= 0)
                return (file->read * 100 / file->size);
        else
                return (-1);
}