1/*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2011 Nathan Whitehorn
5 * Copyright (c) 2014 Devin Teske <dteske@FreeBSD.org>
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 * SUCH DAMAGE.
28 */
29
30#include <sys/param.h>
31
32#include <archive.h>
33#include <ctype.h>
34#include <bsddialog.h>
35#include <bsddialog_progressview.h>
36#include <err.h>
37#include <errno.h>
38#include <limits.h>
39#include <signal.h>
40#include <stdio.h>
41#include <stdlib.h>
42#include <string.h>
43#include <unistd.h>
44
45#include "opt_osname.h"
46
47/* Data to process */
48static const char *distdir = NULL;
49static struct archive *archive = NULL;
50
51/* Function prototypes */
52static void	sig_int(int sig);
53static int	count_files(const char *file);
54static int	extract_files(struct bsddialog_fileminibar *file);
55
56#define _errx(...) (bsddialog_end(), errx(__VA_ARGS__))
57
58int
59main(void)
60{
61	char *chrootdir;
62	char *distributions;
63	char *distribs, *distrib;
64	int retval;
65	size_t minibar_size = sizeof(struct bsddialog_fileminibar);
66	unsigned int nminibars;
67	struct bsddialog_fileminibar *dists;
68	struct bsddialog_progviewconf pvconf;
69	struct bsddialog_conf conf;
70	struct sigaction act;
71	char error[PATH_MAX + 512];
72
73	if ((distributions = getenv("DISTRIBUTIONS")) == NULL)
74		errx(EXIT_FAILURE, "DISTRIBUTIONS variable is not set");
75	if ((distdir = getenv("BSDINSTALL_DISTDIR")) == NULL)
76		distdir = "";
77	if ((distribs = strdup(distributions)) == NULL)
78		errx(EXIT_FAILURE, "memory error");
79
80	if (bsddialog_init() == BSDDIALOG_ERROR)
81		errx(EXIT_FAILURE, "Error libbsdialog: %s",
82		    bsddialog_geterror());
83	bsddialog_initconf(&conf);
84	bsddialog_backtitle(&conf, OSNAME " Installer");
85	bsddialog_infobox(&conf,
86	    "Checking distribution archives.\nPlease wait...", 4, 35);
87
88	/* Parse $DISTRIBUTIONS */
89	nminibars = 0;
90	dists = NULL;
91	while ((distrib = strsep(&distribs, "\t\n\v\f\r ")) != NULL) {
92		if (strlen(distrib) == 0)
93			continue;
94
95		/* Allocate a new struct for the distribution */
96		dists = realloc(dists, (nminibars + 1) * minibar_size);
97		if (dists == NULL)
98			_errx(EXIT_FAILURE, "Out of memory!");
99
100		/* Set file path */
101		dists[nminibars].path = distrib;
102
103		/* Set mini bar label */
104		dists[nminibars].label = strrchr(dists[nminibars].path, '/');
105		if (dists[nminibars].label == NULL)
106			dists[nminibars].label = dists[nminibars].path;
107
108		/* Set initial length in files (-1 == error) */
109		dists[nminibars].size = count_files(dists[nminibars].path);
110		if (dists[nminibars].size < 0) {
111			bsddialog_end();
112			return (EXIT_FAILURE);
113		}
114
115		/* Set initial status and implicitly miniperc to pending */
116		dists[nminibars].status = BSDDIALOG_MG_PENDING;
117
118		/* Set initial read */
119		dists[nminibars].read = 0;
120
121		nminibars += 1;
122	}
123
124	/* Optionally chdir(2) into $BSDINSTALL_CHROOT */
125	chrootdir = getenv("BSDINSTALL_CHROOT");
126	if (chrootdir != NULL && chdir(chrootdir) != 0) {
127		snprintf(error, sizeof(error),
128		    "Could not change to directory %s: %s\n",
129		    chrootdir, strerror(errno));
130		conf.title = "Error";
131		bsddialog_msgbox(&conf, error, 0, 0);
132		bsddialog_end();
133		return (EXIT_FAILURE);
134	}
135
136	/* Set cleanup routine for Ctrl-C action */
137	act.sa_handler = sig_int;
138	sigaction(SIGINT, &act, 0);
139
140	conf.title = "Archive Extraction";
141	conf.auto_minwidth = 40;
142	pvconf.callback	= extract_files;
143	pvconf.refresh = 1;
144	pvconf.fmtbottomstr = "%10lli files read @ %'9.1f files/sec.";
145	bsddialog_total_progview = 0;
146	bsddialog_interruptprogview = bsddialog_abortprogview = false;
147	retval = bsddialog_progressview(&conf,
148	    "\nExtracting distribution files...\n", 0, 0,
149	    &pvconf, nminibars, dists);
150
151	if (retval == BSDDIALOG_ERROR) {
152		fprintf(stderr, "progressview error: %s\n",
153		    bsddialog_geterror());
154	}
155
156	bsddialog_end();
157
158	free(distribs);
159	free(dists);
160
161	return (retval);
162}
163
164static void
165sig_int(int sig __unused)
166{
167	bsddialog_interruptprogview = true;
168}
169
170/*
171 * Returns number of files in archive file. Parses $BSDINSTALL_DISTDIR/MANIFEST
172 * if it exists, otherwise uses archive(3) to read the archive file.
173 */
174static int
175count_files(const char *file)
176{
177	static FILE *manifest = NULL;
178	char *p;
179	int file_count;
180	int retval;
181	size_t span;
182	struct archive_entry *entry;
183	char line[512];
184	char path[PATH_MAX];
185	char errormsg[PATH_MAX + 512];
186	struct bsddialog_conf conf;
187
188	if (manifest == NULL) {
189		snprintf(path, sizeof(path), "%s/MANIFEST", distdir);
190		manifest = fopen(path, "r");
191	}
192
193	if (manifest != NULL) {
194		rewind(manifest);
195		while (fgets(line, sizeof(line), manifest) != NULL) {
196			p = &line[0];
197			span = strcspn(p, "\t") ;
198			if (span < 1 || strncmp(p, file, span) != 0)
199				continue;
200
201			/*
202			 * We're at the right manifest line. The file count is
203			 * in the third element
204			 */
205			span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
206			span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
207			if (span > 0) {
208				file_count = (int)strtol(p, (char **)NULL, 10);
209				if (file_count == 0 && errno == EINVAL)
210					continue;
211				return (file_count);
212			}
213		}
214	}
215
216	/*
217	 * Either no manifest, or manifest didn't mention this archive.
218	 * Use archive(3) to read the archive, counting files within.
219	 */
220	bsddialog_initconf(&conf);
221	if ((archive = archive_read_new()) == NULL) {
222		snprintf(errormsg, sizeof(errormsg),
223		    "Error: %s\n", archive_error_string(NULL));
224		conf.title = "Extract Error";
225		bsddialog_msgbox(&conf, errormsg, 0, 0);
226		return (-1);
227	}
228	archive_read_support_format_all(archive);
229	archive_read_support_filter_all(archive);
230	snprintf(path, sizeof(path), "%s/%s", distdir, file);
231	retval = archive_read_open_filename(archive, path, 4096);
232	if (retval != ARCHIVE_OK) {
233		snprintf(errormsg, sizeof(errormsg),
234		    "Error while extracting %s: %s\n", file,
235		    archive_error_string(archive));
236		conf.title = "Extract Error";
237		bsddialog_msgbox(&conf, errormsg, 0, 0);
238		archive = NULL;
239		return (-1);
240	}
241
242	file_count = 0;
243	while (archive_read_next_header(archive, &entry) == ARCHIVE_OK)
244		file_count++;
245	archive_read_free(archive);
246	archive = NULL;
247
248	return (file_count);
249}
250
251static int
252extract_files(struct bsddialog_fileminibar *file)
253{
254	int retval;
255	struct archive_entry *entry;
256	char path[PATH_MAX];
257	char errormsg[PATH_MAX + 512];
258	struct bsddialog_conf conf;
259
260	bsddialog_initconf(&conf);
261
262	/* Open the archive if necessary */
263	if (archive == NULL) {
264		if ((archive = archive_read_new()) == NULL) {
265			snprintf(errormsg, sizeof(errormsg),
266			    "Error: %s\n", archive_error_string(NULL));
267			conf.title = "Extract Error";
268			bsddialog_msgbox(&conf, errormsg, 0, 0);
269			bsddialog_abortprogview = true;
270			return (-1);
271		}
272		archive_read_support_format_all(archive);
273		archive_read_support_filter_all(archive);
274		snprintf(path, sizeof(path), "%s/%s", distdir, file->path);
275		retval = archive_read_open_filename(archive, path, 4096);
276		if (retval != 0) {
277			snprintf(errormsg, sizeof(errormsg),
278			    "Error opening %s: %s\n", file->label,
279			    archive_error_string(archive));
280			conf.title = "Extract Error";
281			bsddialog_msgbox(&conf, errormsg, 0, 0);
282			file->status = BSDDIALOG_MG_FAILED;
283			bsddialog_abortprogview = true;
284			return (-1);
285		}
286	}
287
288	/* Read the next archive header */
289	retval = archive_read_next_header(archive, &entry);
290
291	/* If that went well, perform the extraction */
292	if (retval == ARCHIVE_OK)
293		retval = archive_read_extract(archive, entry,
294		    ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_OWNER |
295		    ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL |
296		    ARCHIVE_EXTRACT_XATTR | ARCHIVE_EXTRACT_FFLAGS);
297
298	/* Test for either EOF or error */
299	if (retval == ARCHIVE_EOF) {
300		archive_read_free(archive);
301		archive = NULL;
302		file->status = BSDDIALOG_MG_DONE; /*Done*/;
303		return (100);
304	} else if (retval != ARCHIVE_OK &&
305	    !(retval == ARCHIVE_WARN &&
306	    strcmp(archive_error_string(archive), "Can't restore time") == 0)) {
307		/*
308		 * Print any warning/error messages except inability to set
309		 * ctime/mtime, which is not fatal, or even interesting,
310		 * for our purposes. Would be nice if this were a libarchive
311		 * option.
312		 */
313		snprintf(errormsg, sizeof(errormsg),
314		    "Error while extracting %s: %s\n", file->label,
315		    archive_error_string(archive));
316		conf.title = "Extract Error";
317		bsddialog_msgbox(&conf, errormsg, 0, 0);
318		file->status = BSDDIALOG_MG_FAILED; /* Failed */
319		bsddialog_abortprogview = true;
320		return (-1);
321	}
322
323	bsddialog_total_progview++;
324	file->read++;
325
326	/* Calculate [overall] percentage of completion (if possible) */
327	if (file->size >= 0)
328		return (file->read * 100 / file->size);
329	else
330		return (-1);
331}
332