1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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/cdefs.h>
31 __FBSDID("$FreeBSD$");
32 
33 #include <sys/param.h>
34 #include <archive.h>
35 #include <ctype.h>
36 #include <dialog.h>
37 #include <dpv.h>
38 #include <err.h>
39 #include <errno.h>
40 #include <limits.h>
41 #include <stdio.h>
42 #include <stdlib.h>
43 #include <string.h>
44 #include <unistd.h>
45 
46 /* Data to process */
47 static char *distdir = NULL;
48 static struct archive *archive = NULL;
49 static struct dpv_file_node *dists = NULL;
50 
51 /* Function prototypes */
52 static void	sig_int(int sig);
53 static int	count_files(const char *file);
54 static int	extract_files(struct dpv_file_node *file, int out);
55 
56 #if __FreeBSD_version <= 1000008 /* r232154: bump for libarchive update */
57 #define archive_read_support_filter_all(x) \
58 	archive_read_support_compression_all(x)
59 #endif
60 
61 #define _errx(...) (end_dialog(), errx(__VA_ARGS__))
62 
63 int
64 main(void)
65 {
66 	char *chrootdir;
67 	char *distributions;
68 	int retval;
69 	size_t config_size = sizeof(struct dpv_config);
70 	size_t file_node_size = sizeof(struct dpv_file_node);
71 	size_t span;
72 	struct dpv_config *config;
73 	struct dpv_file_node *dist = dists;
74 	static char backtitle[] = "FreeBSD Installer";
75 	static char title[] = "Archive Extraction";
76 	static char aprompt[] = "\n  Overall Progress:";
77 	static char pprompt[] = "Extracting distribution files...\n";
78 	struct sigaction act;
79 	char error[PATH_MAX + 512];
80 
81 	if ((distributions = getenv("DISTRIBUTIONS")) == NULL)
82 		errx(EXIT_FAILURE, "DISTRIBUTIONS variable is not set");
83 	if ((distdir = getenv("BSDINSTALL_DISTDIR")) == NULL)
84 		distdir = __DECONST(char *, "");
85 
86 	/* Initialize dialog(3) */
87 	init_dialog(stdin, stdout);
88 	dialog_vars.backtitle = backtitle;
89 	dlg_put_backtitle();
90 
91 	dialog_msgbox("",
92 	    "Checking distribution archives.\nPlease wait...", 4, 35, FALSE);
93 
94 	/*
95 	 * Parse $DISTRIBUTIONS into dpv(3) linked-list
96 	 */
97 	while (*distributions != '\0') {
98 		span = strcspn(distributions, "\t\n\v\f\r ");
99 		if (span < 1) { /* currently on whitespace */
100 			distributions++;
101 			continue;
102 		}
103 
104 		/* Allocate a new struct for the distribution */
105 		if (dist == NULL) {
106 			if ((dist = calloc(1, file_node_size)) == NULL)
107 				_errx(EXIT_FAILURE, "Out of memory!");
108 			dists = dist;
109 		} else {
110 			dist->next = calloc(1, file_node_size);
111 			if (dist->next == NULL)
112 				_errx(EXIT_FAILURE, "Out of memory!");
113 			dist = dist->next;
114 		}
115 
116 		/* Set path */
117 		if ((dist->path = malloc(span + 1)) == NULL)
118 			_errx(EXIT_FAILURE, "Out of memory!");
119 		snprintf(dist->path, span + 1, "%s", distributions);
120 		dist->path[span] = '\0';
121 
122 		/* Set display name */
123 		dist->name = strrchr(dist->path, '/');
124 		if (dist->name == NULL)
125 			dist->name = dist->path;
126 
127 		/* Set initial length in files (-1 == error) */
128 		dist->length = count_files(dist->path);
129 		if (dist->length < 0) {
130 			end_dialog();
131 			return (EXIT_FAILURE);
132 		}
133 
134 		distributions += span;
135 	}
136 
137 	/* Optionally chdir(2) into $BSDINSTALL_CHROOT */
138 	chrootdir = getenv("BSDINSTALL_CHROOT");
139 	if (chrootdir != NULL && chdir(chrootdir) != 0) {
140 		snprintf(error, sizeof(error),
141 		    "Could not change to directory %s: %s\n",
142 		    chrootdir, strerror(errno));
143 		dialog_msgbox("Error", error, 0, 0, TRUE);
144 		end_dialog();
145 		return (EXIT_FAILURE);
146 	}
147 
148 	/* Set cleanup routine for Ctrl-C action */
149 	act.sa_handler = sig_int;
150 	sigaction(SIGINT, &act, 0);
151 
152 	/*
153 	 * Hand off to dpv(3)
154 	 */
155 	if ((config = calloc(1, config_size)) == NULL)
156 		_errx(EXIT_FAILURE, "Out of memory!");
157 	config->backtitle	= backtitle;
158 	config->title		= title;
159 	config->pprompt		= pprompt;
160 	config->aprompt		= aprompt;
161 	config->options		|= DPV_WIDE_MODE;
162 	config->label_size	= -1;
163 	config->action		= extract_files;
164 	config->status_solo	=
165 	    "%10lli files read @ %'9.1f files/sec.";
166 	config->status_many	=
167 	    "%10lli files read @ %'9.1f files/sec. [%i/%i busy/wait]";
168 	end_dialog();
169 	retval = dpv(config, dists);
170 
171 	dpv_free();
172 	while ((dist = dists) != NULL) {
173 		dists = dist->next;
174 		if (dist->path != NULL)
175 			free(dist->path);
176 		free(dist);
177 	}
178 
179 	return (retval);
180 }
181 
182 static void
183 sig_int(int sig __unused)
184 {
185 	dpv_interrupt = TRUE;
186 }
187 
188 /*
189  * Returns number of files in archive file. Parses $BSDINSTALL_DISTDIR/MANIFEST
190  * if it exists, otherwise uses archive(3) to read the archive file.
191  */
192 static int
193 count_files(const char *file)
194 {
195 	static FILE *manifest = NULL;
196 	char *p;
197 	int file_count;
198 	int retval;
199 	size_t span;
200 	struct archive_entry *entry;
201 	char line[512];
202 	char path[PATH_MAX];
203 	char errormsg[PATH_MAX + 512];
204 
205 	if (manifest == NULL) {
206 		snprintf(path, sizeof(path), "%s/MANIFEST", distdir);
207 		manifest = fopen(path, "r");
208 	}
209 
210 	if (manifest != NULL) {
211 		rewind(manifest);
212 		while (fgets(line, sizeof(line), manifest) != NULL) {
213 			p = &line[0];
214 			span = strcspn(p, "\t") ;
215 			if (span < 1 || strncmp(p, file, span) != 0)
216 				continue;
217 
218 			/*
219 			 * We're at the right manifest line. The file count is
220 			 * in the third element
221 			 */
222 			span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
223 			span = strcspn(p += span + (*p != '\0' ? 1 : 0), "\t");
224 			if (span > 0) {
225 				file_count = (int)strtol(p, (char **)NULL, 10);
226 				if (file_count == 0 && errno == EINVAL)
227 					continue;
228 				return (file_count);
229 			}
230 		}
231 	}
232 
233 	/*
234 	 * Either no manifest, or manifest didn't mention this archive.
235 	 * Use archive(3) to read the archive, counting files within.
236 	 */
237 	if ((archive = archive_read_new()) == NULL) {
238 		snprintf(errormsg, sizeof(errormsg),
239 		    "Error: %s\n", archive_error_string(NULL));
240 		dialog_msgbox("Extract Error", errormsg, 0, 0, TRUE);
241 		return (-1);
242 	}
243 	archive_read_support_format_all(archive);
244 	archive_read_support_filter_all(archive);
245 	snprintf(path, sizeof(path), "%s/%s", distdir, file);
246 	retval = archive_read_open_filename(archive, path, 4096);
247 	if (retval != ARCHIVE_OK) {
248 		snprintf(errormsg, sizeof(errormsg),
249 		    "Error while extracting %s: %s\n", file,
250 		    archive_error_string(archive));
251 		dialog_msgbox("Extract Error", errormsg, 0, 0, TRUE);
252 		archive = NULL;
253 		return (-1);
254 	}
255 
256 	file_count = 0;
257 	while (archive_read_next_header(archive, &entry) == ARCHIVE_OK)
258 		file_count++;
259 	archive_read_free(archive);
260 	archive = NULL;
261 
262 	return (file_count);
263 }
264 
265 static int
266 extract_files(struct dpv_file_node *file, int out __unused)
267 {
268 	int retval;
269 	struct archive_entry *entry;
270 	char path[PATH_MAX];
271 	char errormsg[PATH_MAX + 512];
272 
273 	/* Open the archive if necessary */
274 	if (archive == NULL) {
275 		if ((archive = archive_read_new()) == NULL) {
276 			snprintf(errormsg, sizeof(errormsg),
277 			    "Error: %s\n", archive_error_string(NULL));
278 			dialog_msgbox("Extract Error", errormsg, 0, 0, TRUE);
279 			dpv_abort = 1;
280 			return (-1);
281 		}
282 		archive_read_support_format_all(archive);
283 		archive_read_support_filter_all(archive);
284 		snprintf(path, sizeof(path), "%s/%s", distdir, file->path);
285 		retval = archive_read_open_filename(archive, path, 4096);
286 		if (retval != 0) {
287 			snprintf(errormsg, sizeof(errormsg),
288 			    "Error opening %s: %s\n", file->name,
289 			    archive_error_string(archive));
290 			dialog_msgbox("Extract Error", errormsg, 0, 0, TRUE);
291 			file->status = DPV_STATUS_FAILED;
292 			dpv_abort = 1;
293 			return (-1);
294 		}
295 	}
296 
297 	/* Read the next archive header */
298 	retval = archive_read_next_header(archive, &entry);
299 
300 	/* If that went well, perform the extraction */
301 	if (retval == ARCHIVE_OK)
302 		retval = archive_read_extract(archive, entry,
303 		    ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_OWNER |
304 		    ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL |
305 		    ARCHIVE_EXTRACT_XATTR | ARCHIVE_EXTRACT_FFLAGS);
306 
307 	/* Test for either EOF or error */
308 	if (retval == ARCHIVE_EOF) {
309 		archive_read_free(archive);
310 		archive = NULL;
311 		file->status = DPV_STATUS_DONE;
312 		return (100);
313 	} else if (retval != ARCHIVE_OK &&
314 	    !(retval == ARCHIVE_WARN &&
315 	    strcmp(archive_error_string(archive), "Can't restore time") == 0)) {
316 		/*
317 		 * Print any warning/error messages except inability to set
318 		 * ctime/mtime, which is not fatal, or even interesting,
319 		 * for our purposes. Would be nice if this were a libarchive
320 		 * option.
321 		 */
322 		snprintf(errormsg, sizeof(errormsg),
323 		    "Error while extracting %s: %s\n", file->name,
324 		    archive_error_string(archive));
325 		dialog_msgbox("Extract Error", errormsg, 0, 0, TRUE);
326 		file->status = DPV_STATUS_FAILED;
327 		dpv_abort = 1;
328 		return (-1);
329 	}
330 
331 	dpv_overall_read++;
332 	file->read++;
333 
334 	/* Calculate [overall] percentage of completion (if possible) */
335 	if (file->length >= 0)
336 		return (file->read * 100 / file->length);
337 	else
338 		return (-1);
339 }
340