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