1 /* vim: set sw=4 ts=4 noexpandtab : */
2 /*
3  * Copyright (C) 2007-2019 Abel Cheung.
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  * 3. Neither the name of the project nor the names of its contributors
15  *    may be used to endorse or promote products derived from this software
16  *    without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
19  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
22  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28  * SUCH DAMAGE.
29  */
30 
31 #include "config.h"
32 
33 #include <errno.h>
34 #include <stdlib.h>
35 
36 #include "utils.h"
37 #ifdef G_OS_WIN32
38 #  include "utils-win.h"
39 #endif
40 
41 #include <glib/gi18n.h>
42 #include <glib/gstdio.h>
43 
44 #include "rifiuti-vista.h"
45 
46 static r2status     exit_status          = EXIT_SUCCESS;
47 static metarecord   meta;
48 
49 
50 /*!
51  * Check if index file has sufficient amount of data for reading.
52  * If successful, its file content will be stored in buf.
53  * Return 0 if successful, non-zero on error
54  */
55 static r2status
validate_index_file(const char * filename,void ** filebuf,gsize * bufsize,uint64_t * ver,uint32_t * pathlen)56 validate_index_file (const char  *filename,
57                      void       **filebuf,
58                      gsize       *bufsize,
59                      uint64_t    *ver,
60                      uint32_t    *pathlen)
61 {
62 	gsize           expected;
63 	int             status;
64 	GError         *err = NULL;
65 	char           *buf;
66 
67 	g_debug ("Start file validation for '%s'...", filename);
68 
69 	g_return_val_if_fail ( (filename != NULL) && (*filename != '\0'),
70 	                       R2_ERR_INTERNAL );
71 	g_return_val_if_fail ( (filebuf  != NULL), R2_ERR_INTERNAL );
72 	g_return_val_if_fail ( (bufsize  != NULL), R2_ERR_INTERNAL );
73 	g_return_val_if_fail ( (ver      != NULL), R2_ERR_INTERNAL );
74 	g_return_val_if_fail ( (pathlen  != NULL), R2_ERR_INTERNAL );
75 
76 	if ( !g_file_get_contents (filename, &buf, bufsize, &err) )
77 	{
78 		g_critical (_("%s(): failed to retrieve file content for '%s': %s"),
79 			__func__, filename, err->message);
80 		g_clear_error (&err);
81 		status = R2_ERR_OPEN_FILE;
82 		goto validation_error;
83 	}
84 
85 	g_debug ("Retrieval of '%s' is done, size = %" G_GSIZE_FORMAT, filename, *bufsize);
86 
87 	if (*bufsize <= VERSION1_FILENAME_OFFSET)
88 	{
89 		g_debug ("File size expected to be more than %" G_GSIZE_FORMAT,
90 			(gsize) VERSION1_FILENAME_OFFSET);
91 		g_printerr (_("File is truncated, or probably not a $Recycle.bin index file."));
92 		g_printerr ("\n");
93 		status = R2_ERR_BROKEN_FILE;
94 		goto validation_error;
95 	}
96 
97 	copy_field (ver, VERSION, FILESIZE);
98 	*ver = GUINT64_FROM_LE (*ver);
99 	g_debug ("version = %" G_GUINT64_FORMAT, *ver);
100 
101 	switch (*ver)
102 	{
103 		case VERSION_VISTA:
104 
105 			expected = VERSION1_FILE_SIZE;
106 			/* see populate_record_data() for reason */
107 			if ( (*bufsize != expected) && (*bufsize != expected - 1) )
108 			{
109 				g_debug ("File size expected to be %" G_GSIZE_FORMAT
110 					" or %" G_GSIZE_FORMAT, expected, expected - 1);
111 				g_printerr (_("Index file expected size and real size do not match."));
112 				g_printerr ("\n");
113 				status = R2_ERR_BROKEN_FILE;
114 				goto validation_error;
115 			}
116 			*pathlen = WIN_PATH_MAX;
117 			break;
118 
119 		case VERSION_WIN10:
120 
121 			copy_field (pathlen, VERSION1_FILENAME, VERSION2_FILENAME);
122 			*pathlen = GUINT32_FROM_LE (*pathlen);
123 
124 			/* Header length + file name length in UTF-16 encoding */
125 			expected = VERSION2_FILENAME_OFFSET + (*pathlen) * 2;
126 			if (*bufsize != expected)
127 			{
128 				g_debug ("File size expected to be %" G_GSIZE_FORMAT, expected);
129 				g_printerr (_("Index file expected size and real size do not match."));
130 				g_printerr ("\n");
131 				status = R2_ERR_BROKEN_FILE;
132 				goto validation_error;
133 			}
134 			break;
135 
136 		default:
137 			g_printerr (_("Unsupported file version, or probably not a $Recycle.bin index file."));
138 			g_printerr ("\n");
139 			status = R2_ERR_BROKEN_FILE;
140 			goto validation_error;
141 	}
142 
143 	*filebuf = buf;
144 	return EXIT_SUCCESS;
145 
146 validation_error:
147 
148 	*filebuf = NULL;
149 	return status;
150 }
151 
152 
153 static rbin_struct *
populate_record_data(void * buf,uint64_t version,uint32_t pathlen,gboolean erraneous)154 populate_record_data (void *buf,
155                       uint64_t version,
156                       uint32_t pathlen,
157                       gboolean erraneous)
158 {
159 	uint64_t      win_filetime;
160 	rbin_struct  *record;
161 	size_t        read;
162 
163 	record = g_malloc0 (sizeof (rbin_struct));
164 	record->version = version;
165 
166 	/*
167 	 * In rare cases, the size of index file is 543 bytes versus (normal) 544 bytes.
168 	 * In such occasion file size only occupies 56 bit, not 64 bit as it ought to be.
169 	 * Actually this 56-bit file size is very likely wrong after all. Probably some
170 	 * bug inside Windows. This is observed during deletion of dd.exe from Forensic
171 	 * Acquisition Utilities (by George M. Garner Jr) in certain localized Vista.
172 	 */
173 	memcpy (&record->filesize, buf + FILESIZE_OFFSET,
174 			FILETIME_OFFSET - FILESIZE_OFFSET - (int) erraneous);
175 	if (erraneous)
176 	{
177 		g_debug ("filesize field broken, 56 bit only, val=0x%" G_GINT64_MODIFIER "X",
178 		         record->filesize);
179 		/* not printing the value because it was wrong and misleading */
180 		record->filesize = G_MAXUINT64;
181 	}
182 	else
183 	{
184 		record->filesize = GUINT64_FROM_LE (record->filesize);
185 		g_debug ("deleted file size = %" G_GUINT64_FORMAT, record->filesize);
186 	}
187 
188 	/* File deletion time */
189 	memcpy (&win_filetime, buf + FILETIME_OFFSET - (int) erraneous,
190 	        VERSION1_FILENAME_OFFSET - FILETIME_OFFSET);
191 	win_filetime = GUINT64_FROM_LE (win_filetime);
192 	record->deltime = win_filetime_to_epoch (win_filetime);
193 
194 	/* One extra char for safety, though path should have already been null terminated */
195 	g_debug ("pathlen = %d", pathlen);
196 	switch (version)
197 	{
198 		case VERSION_VISTA:
199 			record->uni_path = conv_path_to_utf8_with_tmpl (
200 				(const char *) (buf - erraneous + VERSION1_FILENAME_OFFSET),
201 				NULL, "<\\u%04X>", &read, &exit_status);
202 			break;
203 
204 		case VERSION_WIN10:
205 			record->uni_path = conv_path_to_utf8_with_tmpl (
206 				(const char *) (buf + VERSION2_FILENAME_OFFSET),
207 				NULL, "<\\u%04X>", &read, &exit_status);
208 			break;
209 
210 		default:
211 			g_assert_not_reached ();
212 	}
213 
214 	if (record->uni_path == NULL) {
215 		g_warning (_("(Record %s) Error converting unicode path to UTF-8."),
216 			record->index_s);
217 		record->uni_path = "";
218 	}
219 
220 	return record;
221 }
222 
223 static void
parse_record_cb(char * index_file,GSList ** recordlist)224 parse_record_cb (char    *index_file,
225                  GSList **recordlist)
226 {
227 	rbin_struct    *record;
228 	char           *basename;
229 	uint64_t        version = 0;
230 	uint32_t        pathlen = 0;
231 	gsize           bufsize;
232 	void           *buf = NULL;
233 	r2status        validate_st;
234 
235 	basename = g_path_get_basename (index_file);
236 
237 	validate_st = validate_index_file (
238 		index_file, &buf, &bufsize, &version, &pathlen);
239 	if ( validate_st != EXIT_SUCCESS )
240 	{
241 		g_printerr (_("File '%s' fails validation."), basename);
242 		g_printerr ("\n");
243 		exit_status = validate_st;
244 		goto parse_record_error;
245 	}
246 
247 	g_debug ("Start populating record for '%s'...", basename);
248 
249 	switch (version)
250 	{
251 		case VERSION_VISTA:
252 			/* see populate_record_data() for meaning of last parameter */
253 			record = populate_record_data (buf, version, pathlen,
254 										(bufsize == VERSION1_FILE_SIZE - 1));
255 			break;
256 
257 		case VERSION_WIN10:
258 			record = populate_record_data (buf, version, pathlen, FALSE);
259 			break;
260 
261 		default:
262 			g_assert_not_reached();
263 	}
264 
265 	g_debug ("Parsing done for '%s'", basename);
266 	record->index_s = basename;
267 	record->meta = &meta;
268 	*recordlist = g_slist_prepend (*recordlist, record);
269 	g_free (buf);
270 	return;
271 
272 	parse_record_error:
273 
274 	g_free (buf);
275 	g_free (basename);
276 }
277 
278 
279 static int
sort_record_by_time(rbin_struct * a,rbin_struct * b)280 sort_record_by_time (rbin_struct *a,
281                      rbin_struct *b)
282 {
283 	/* sort primary key: deletion time; secondary key: index file name */
284 	return ((a->deltime < b->deltime) ? -1 :
285 	        (a->deltime > b->deltime) ?  1 :
286 	        strcmp (a->index_s, b->index_s));
287 }
288 
289 
290 int
main(int argc,char ** argv)291 main (int    argc,
292       char **argv)
293 {
294 	GSList             *filelist   = NULL;
295 	GSList             *recordlist = NULL;
296 	GOptionContext     *context;
297 
298 	extern char       **fileargs;
299 
300 	rifiuti_init (argv[0]);
301 
302 	/* TRANSLATOR: appears in help text short summary */
303 	context = g_option_context_new (N_("DIR_OR_FILE"));
304 	g_option_context_set_summary (context, N_(
305 		"Parse index files in C:\\$Recycle.bin style folder "
306 		"and dump recycle bin data.  Can also dump a single index file."));
307 	rifiuti_setup_opt_ctx (&context, RECYCLE_BIN_TYPE_DIR);
308 	exit_status = rifiuti_parse_opt_ctx (&context, &argc, &argv);
309 	if (exit_status != EXIT_SUCCESS)
310 		goto cleanup;
311 
312 	exit_status = check_file_args (fileargs[0], &filelist, RECYCLE_BIN_TYPE_DIR);
313 	if (exit_status != EXIT_SUCCESS)
314 		goto cleanup;
315 
316 	g_slist_foreach (filelist, (GFunc) parse_record_cb, &recordlist);
317 
318 	/* Fill in recycle bin metadata */
319 	meta.type               = RECYCLE_BIN_TYPE_DIR;
320 	meta.filename           = fileargs[0];
321 	meta.keep_deleted_entry = FALSE;
322 	meta.is_empty           = (filelist == NULL);
323 	meta.has_unicode_path   = TRUE;
324 
325 	/* NULL filelist at this point means a valid empty $Recycle.bin */
326 	if ( !meta.is_empty && (recordlist == NULL) )
327 	{
328 		g_printerr (_("No valid recycle bin index file found."));
329 		g_printerr ("\n");
330 		exit_status = R2_ERR_BROKEN_FILE;
331 		goto cleanup;
332 	}
333 	recordlist = g_slist_sort (recordlist, (GCompareFunc) sort_record_by_time);
334 
335 	/* detect global recycle bin version from versions of all files */
336 	{
337 		GSList  *l = recordlist;
338 		if (!l)
339 			meta.version = VERSION_NOT_FOUND;
340 		else
341 		{
342 			meta.version = (int64_t) ((rbin_struct *) recordlist->data)->version;
343 
344 			while (NULL != (l = l->next))
345 			{
346 				if ((int64_t) ((rbin_struct *) l->data)->version != meta.version)
347 				{
348 					meta.version = VERSION_INCONSISTENT;
349 					break;
350 				}
351 			}
352 
353 			if (meta.version == VERSION_INCONSISTENT)
354 			{
355 				g_printerr (_("Index files come from multiple versions of Windows."
356 					"  Please check each file independently."));
357 				g_printerr ("\n");
358 				exit_status = R2_ERR_BROKEN_FILE;
359 				goto cleanup;
360 			}
361 		}
362 	}
363 
364 	/*
365 	 * No attempt is made to distinguish difference for Vista - 8.1.
366 	 * The corrupt filesize artifact on Vista can't be reproduced,
367 	 * therefore must be very rare.
368 	 */
369 	switch (meta.version)
370 	{
371 		case VERSION_VISTA: meta.os_guess = OS_GUESS_VISTA; break;
372 		case VERSION_WIN10: meta.os_guess = OS_GUESS_10;    break;
373 		default:            meta.os_guess = OS_GUESS_UNKNOWN;
374 	}
375 
376 	/* Print everything */
377 	{
378 		r2status s = prepare_output_handle ();
379 		if (s != EXIT_SUCCESS) {
380 			exit_status = s;
381 			goto cleanup;
382 		}
383 	}
384 
385 	print_header (meta);
386 	g_slist_foreach (recordlist, (GFunc) print_record_cb, NULL);
387 	print_footer ();
388 
389 	close_output_handle ();
390 
391 	/* file descriptor should have been closed at this point */
392 	{
393 		r2status s = move_temp_file ();
394 		if ( s != EXIT_SUCCESS )
395 			exit_status = s;
396 	}
397 
398 	cleanup:
399 
400 	/* Last minute error messages for accumulated non-fatal errors */
401 	switch (exit_status)
402 	{
403 		case R2_ERR_USER_ENCODING:
404 			g_printerr (_("Some entries could not be presented as correct "
405 				"unicode path.  The concerned characters are displayed "
406 				"in escaped unicode sequences."));
407 			g_printerr ("\n");
408 			break;
409 
410 		default:
411 			break;
412 	}
413 
414 	g_debug ("Cleaning up...");
415 
416 	g_slist_free_full (recordlist, (GDestroyNotify) free_record_cb);
417 	g_slist_free_full (filelist  , (GDestroyNotify) g_free        );
418 	free_vars ();
419 
420 	return exit_status;
421 }
422