1 /*
2  *
3  * CLEX File Manager
4  *
5  * Copyright (C) 2001-2018 Vlado Potisk <vlado_potisk@clex.sk>
6  *
7  * CLEX is free software without warranty of any kind; see the
8  * GNU General Public License as set out in the "COPYING" document
9  * which accompanies the CLEX File Manager package.
10  *
11  * CLEX can be downloaded from http://www.clex.sk
12  *
13  */
14 
15 /* comparing the contents of two directories (filepanels) */
16 
17 #include "clexheaders.h"
18 
19 #include <sys/stat.h>	/* stat() */
20 #include <errno.h>		/* errno */
21 #include <fcntl.h>		/* open() */
22 #include <stdarg.h>		/* log.h */
23 #include <stdlib.h>		/* qsort() */
24 #include <string.h>		/* strcmp() */
25 #include <unistd.h>		/* close() */
26 
27 #include "select.h"
28 
29 #include "inout.h"		/* win_waitmsg() */
30 #include "list.h"		/* list_both_directories() */
31 #include "log.h"		/* msgout() */
32 #include "opt.h"		/* opt_changed() */
33 #include "panel.h"		/* pan_adjust() */
34 #include "signals.h"	/* signal_ctrlc_on() */
35 #include "util.h"		/* pathname_join() */
36 
37 int
cmp_prepare(void)38 cmp_prepare(void)
39 {
40 	if (strcmp(USTR(ppanel_file->dir),USTR(ppanel_file->other->dir)) == 0) {
41 		msgout(MSG_i,"COMPARE: same directory in both panels");
42 		return -1;
43 	}
44 
45 	panel_cmp.pd->top = panel_fopt.pd->min;
46 	panel_cmp.pd->curs = panel_cmp.pd->cnt - 1;	/* last line */
47 	panel = panel_cmp.pd;
48 	pan_adjust(panel);
49 	textline = 0;
50 
51 	return 0;
52 }
53 
54 int
cmp_summary_prepare(void)55 cmp_summary_prepare(void)
56 {
57 	panel_cmp_sum.pd->top = panel_cmp_sum.pd->curs = panel_cmp_sum.pd->min;
58 	panel_cmp_sum.pd->cnt = panel_cmp_sum.errors ? 6 : 5;
59 	panel = panel_cmp_sum.pd;
60 	textline = 0;
61 
62 	if (panel_cmp_sum.errors)
63 		win_sethelp(HELPMSG_BASE,L"Error messages can be found in the log (alt-L)");
64 
65 	return 0;
66 }
67 
68 /* write options to a string */
69 const char *
cmp_saveopt(void)70 cmp_saveopt(void)
71 {
72 	int i, j;
73 	static char buff[CMP_TOTAL_ + 1];
74 
75 	for (i = j = 0; i < CMP_TOTAL_; i++)
76 		if (COPT(i))
77 			buff[j++] = 'A' + i;
78 	buff[j] = '\0';
79 
80 	return buff;
81 }
82 
83 /* read options from a string */
84 int
cmp_restoreopt(const char * opt)85 cmp_restoreopt(const char *opt)
86 {
87 	int i;
88 	unsigned char ch;
89 
90 	for (i = 0; i < CMP_TOTAL_; i++)
91 		COPT(i) = 0;
92 
93 	while ( (ch = *opt++) ) {
94 		if (ch < 'A' || ch >= 'A' + CMP_TOTAL_)
95 			return -1;
96 		COPT(ch - 'A') = 1;
97 	}
98 
99 	return 0;
100 }
101 
102 static int
qcmp(const void * e1,const void * e2)103 qcmp(const void *e1, const void *e2)
104 {
105 	return strcmp( /* not strcoll() */
106 	  SDSTR((*(FILE_ENTRY **)e1)->file),
107 	  SDSTR((*(FILE_ENTRY **)e2)->file));
108 }
109 
110 #define CMP_BUF_STR	16384
111 
112 /* return value: -1 error, 0 compare ok, +1 compare failed */
113 static int
data_cmp(int fd1,const char * file1,int fd2,const char * file2)114 data_cmp(int fd1, const char *file1, int fd2, const char *file2)
115 {
116 	struct stat st1, st2;
117 	static char buff1[CMP_BUF_STR], buff2[CMP_BUF_STR];
118 	off_t filesize;
119 	size_t chunksize;
120 
121 	if (fstat(fd1,&st1) < 0 || !S_ISREG(st1.st_mode)) {
122 		msgout(MSG_NOTICE,"COMPARE: File \"./%s\" is not a regular file",file1);
123 		return -1;
124 	}
125 	if (fstat(fd2,&st2) < 0 || !S_ISREG(st2.st_mode)) {
126 		msgout(MSG_NOTICE,"COMPARE: File \"%s\" is not a regular file",file2);
127 		return -1;
128 	}
129 	if (st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino)
130 		/* same file */
131 		return 0;
132 	if ((filesize = st1.st_size) != st2.st_size)
133 		return 1;
134 
135 	while (filesize > 0) {
136 		chunksize = filesize > CMP_BUF_STR ? CMP_BUF_STR : filesize;
137 		if (ctrlc_flag)
138 			return -1;
139 		if (read_fd(fd1,buff1,chunksize) != chunksize) {
140 			msgout(MSG_NOTICE,"COMPARE: Cannot read from \"./%s\" (%s)",file1,strerror(errno));
141 			return -1;
142 		}
143 		if (read_fd(fd2,buff2,chunksize) != chunksize) {
144 			msgout(MSG_NOTICE,"COMPARE: Cannot read from \"%s\" (%s)",file2,strerror(errno));
145 			return -1;
146 		}
147 		if (memcmp(buff1,buff2,chunksize) != 0)
148 			return 1;
149 		filesize -= chunksize;
150 	}
151 
152 	return 0;
153 }
154 
155 /* return value: -1 error, 0 compare ok, +1 compare failed */
156 static int
file_cmp(const char * file1,const char * file2)157 file_cmp(const char *file1, const char *file2)
158 {
159 	int cmp, fd1, fd2;
160 
161 	fd1 = open(file1,O_RDONLY | O_NONBLOCK);
162 	if (fd1 < 0) {
163 		msgout(MSG_NOTICE,"COMPARE: Cannot open \"./%s\" (%s)",file1,strerror(errno));
164 		return -1;
165 	}
166 	fd2 = open(file2,O_RDONLY | O_NONBLOCK);
167 	if (fd2 < 0) {
168 		msgout(MSG_NOTICE,"COMPARE: Cannot open \"%s\" (%s)",file2,strerror(errno));
169 		close(fd1);
170 		return -1;
171 	}
172 	cmp = data_cmp(fd1,file1,fd2,file2);
173 	close(fd1);
174 	close(fd2);
175 	return cmp;
176 }
177 
178 static void
cmp_directories(void)179 cmp_directories(void)
180 {
181 	int min, med, max, cmp, i, j, cnt1, selcnt1, selcnt2;
182 	const char *name2;
183 	FILE_ENTRY *pfe1, *pfe2;
184 	static FILE_ENTRY **p1 = 0;	/* copy of panel #1 sorted for binary search */
185 	static int p1_alloc = 0;
186 
187 	/*
188 	 * - select all files and both panels
189 	 * - for each file from panel #2:
190 	 *		- find a matching file from panel #1
191 	 *		- if a pair is found, compare them according to the selected options
192 	 *		- if the compared files are equal, deselect them
193 	 */
194 
195 	panel_cmp_sum.errors = panel_cmp_sum.names = panel_cmp_sum.equal = 0;
196 
197 	/* reread panels */
198 	list_both_directories();
199 
200 	ctrlc_flag = 0;
201 	if (COPT(CMP_DATA)) {	/* going to compare data */
202 		signal_ctrlc_on();
203 		win_waitmsg();
204 		pathname_set_directory(USTR(ppanel_file->other->dir));
205 	}
206 
207 	if (COPT(CMP_REGULAR)) {
208 		for (cnt1 = i = 0; i < ppanel_file->pd->cnt; i++) {
209 			pfe1 = ppanel_file->files[i];
210 			if ( (pfe1->select = IS_FT_PLAIN(pfe1->file_type)) )
211 				cnt1++;
212 		}
213 		panel_cmp_sum.nonreg1 = ppanel_file->pd->cnt - cnt1;
214 	}
215 	else {
216 		cnt1 = ppanel_file->pd->cnt;
217 		panel_cmp_sum.nonreg1 = 0;
218 	}
219 	selcnt1 = cnt1;
220 
221 	if (cnt1) {
222 		if (p1_alloc < cnt1) {
223 			efree(p1);
224 			p1_alloc = cnt1;
225 			p1 = emalloc(p1_alloc * sizeof(FILE_ENTRY *));
226 		}
227 
228 		if (COPT(CMP_REGULAR))
229 			for (i = j = 0; i < ppanel_file->pd->cnt; i++) {
230 				pfe1 = ppanel_file->files[i];
231 				if (pfe1->select)
232 					p1[j++] = pfe1;
233 			}
234 		else
235 			for (i = 0; i < ppanel_file->pd->cnt; i++) {
236 				pfe1 = p1[i] = ppanel_file->files[i];
237 				pfe1->select = 1;
238 		}
239 
240 		qsort(p1,cnt1,sizeof(FILE_ENTRY *),qcmp);
241 	}
242 
243 	panel_cmp_sum.nonreg2 = 0;
244 	selcnt2 = 0;
245 	for (i = 0; i < ppanel_file->other->pd->cnt; i++) {
246 		pfe2 = ppanel_file->other->files[i];
247 		if ( !(pfe2->select = !COPT(CMP_REGULAR) || IS_FT_PLAIN(pfe2->file_type)) ) {
248 			panel_cmp_sum.nonreg2++;
249 			continue;
250 		}
251 		selcnt2++;
252 
253 		if (panel_cmp_sum.names == cnt1)
254 			/* we have seen all files from panel#1 */
255 			continue;	/* not break */
256 
257 		name2 = SDSTR(pfe2->file);
258 		for (pfe1 = 0, min = 0, max = cnt1 - 1; min <= max; ) {
259 			med = (min + max) / 2;
260 			cmp = strcmp(name2,SDSTR(p1[med]->file));
261 			if (cmp == 0) {
262 				pfe1 = p1[med];
263 				/* entries *pfe1 and *pfe2 have the same name */
264 				break;
265 			}
266 			if (cmp < 0)
267 				max = med - 1;
268 			else
269 				min = med + 1;
270 		}
271 		if (pfe1 == 0 || !pfe1->select)
272 			continue;
273 
274 		panel_cmp_sum.names++;
275 
276 		/* always comparing type */
277 		if (pfe1->file_type == FT_NA || !(
278 			(IS_FT_PLAIN(pfe1->file_type) && IS_FT_PLAIN(pfe2->file_type))
279 			|| (IS_FT_DIR(pfe1->file_type) && IS_FT_DIR(pfe2->file_type))
280 			|| (pfe1->file_type == pfe2->file_type) )
281 		  )
282 			continue;
283 		if (pfe1->symlink != pfe2->symlink)
284 			continue;
285 
286 		/* comparing size (or device numbers) */
287 		if (COPT(CMP_SIZE)
288 		  && ((IS_FT_DEV(pfe1->file_type) && pfe1->devnum != pfe2->devnum)
289 		  || (IS_FT_PLAIN(pfe1->file_type) && pfe1->size != pfe2->size)))
290 			continue;
291 
292 		if (COPT(CMP_OWNER)
293 		  && (pfe1->uid != pfe2->uid || pfe1->gid != pfe2->gid))
294 			continue;
295 
296 		if (COPT(CMP_MODE) && pfe1->mode12 != pfe2->mode12)
297 			continue;
298 
299 		if (COPT(CMP_DATA) && IS_FT_PLAIN(pfe1->file_type)) {
300 			if (pfe1->size != pfe2->size)
301 				continue;
302 			if ( (cmp = file_cmp(SDSTR(pfe1->file),pathname_join(name2))) ) {
303 				if (ctrlc_flag)
304 					break;
305 				if (cmp < 0)
306 					panel_cmp_sum.errors++;
307 				continue;
308 			}
309 		}
310 
311 		/* pair of matching files found */
312 		pfe1->select = 0;
313 		selcnt1--;
314 		pfe2->select = 0;
315 		selcnt2--;
316 		panel_cmp_sum.equal++;
317 	}
318 	ppanel_file->selected = selcnt1;
319 	ppanel_file->other->selected = selcnt2;
320 
321 	if (COPT(CMP_DATA))
322 		signal_ctrlc_off();
323 
324 	if (ctrlc_flag) {
325 		msgout(MSG_i,"COMPARE: operation canceled");
326 		/* clear all marks */
327 		for (i = 0; i < cnt1; i++)
328 			ppanel_file->files[i]->select = 0;
329 		ppanel_file->selected = 0;
330 		for (i = 0; i < ppanel_file->other->pd->cnt; i++)
331 			ppanel_file->other->files[i]->select = 0;
332 		ppanel_file->other->selected = 0;
333 
334 		next_mode = MODE_SPECIAL_RETURN;
335 		return;
336 	}
337 
338 	next_mode = MODE_CMP_SUM;
339 }
340 
341 void
cx_cmp(void)342 cx_cmp(void)
343 {
344 	int sel;
345 
346 	sel = panel_cmp.pd->curs;
347 	if (sel < CMP_TOTAL_) {
348 		TOGGLE(COPT(sel));
349 		opt_changed();
350 		win_panel_opt();
351 	}
352 	else
353 		cmp_directories();
354 }
355