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 */
45 static char *distdir = NULL;
46 static struct archive *archive = NULL;
47 static struct dpv_file_node *dists = NULL;
48 
49 /* Function prototypes */
50 static void	sig_int(int sig);
51 static int	count_files(const char *file);
52 static 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 
61 int
62 main(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 
180 static void
181 sig_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  */
190 static int
191 count_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 
263 static int
264 extract_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