1 /*
2  * Copyright 2017 LarsGit223
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include <glib.h>
20 #include <glib/gstdio.h>
21 
22 #ifdef HAVE_CONFIG_H
23 # include "config.h"
24 #endif
25 
26 #include "filelist.h"
27 #include <geanyplugin.h>
28 
29 typedef struct
30 {
31 	guint file_count;
32 	guint folder_count;
33 	GSList *filelist;
34 	GSList *file_patterns;
35 	GSList *ignored_dirs_list;
36 	GSList *ignored_file_list;
37 	GHashTable *visited_paths;
38 }
39 ScanDirParams;
40 
41 
42 typedef struct
43 {
44 	GSList *filelist;
45 	GHashTable *visited_paths;
46 	void (*callback)(const gchar *path, gboolean *add, gboolean *enter, void *userdata);
47 	void *userdata;
48 }
49 ScanDirParamsCallback;
50 
51 
52 /** Get precompiled patterns.
53  *
54  * The function builds the precompiled patterns for @a patterns and returns them
55  * as a list.
56  *
57  * @param patterns NULL terminated string array of patterns.
58  * @return Pointer to GSList of patterns or NULL if patterns == NULL
59  *
60  **/
filelist_get_precompiled_patterns(gchar ** patterns)61 GSList *filelist_get_precompiled_patterns(gchar **patterns)
62 {
63 	guint i;
64 	GSList *pattern_list = NULL;
65 
66 	if (!patterns)
67 		return NULL;
68 
69 	for (i = 0; patterns[i] != NULL; i++)
70 	{
71 		GPatternSpec *pattern_spec = g_pattern_spec_new(patterns[i]);
72 		pattern_list = g_slist_prepend(pattern_list, pattern_spec);
73 	}
74 	return pattern_list;
75 }
76 
77 
78 /** Check if a string matches a pattern.
79  *
80  * The function checks if @a str matches pattern @a patterns.
81  *
82  * @param patterns Pattern list.
83  * @param str      String to check
84  * @return TRUE if str matches the pattern, FALSE otherwise
85  *
86  **/
filelist_patterns_match(GSList * patterns,const gchar * str)87 gboolean filelist_patterns_match(GSList *patterns, const gchar *str)
88 {
89 	GSList *elem = NULL;
90 	foreach_slist (elem, patterns)
91 	{
92 		GPatternSpec *pattern = elem->data;
93 		if (g_pattern_match_string(pattern, str))
94 			return TRUE;
95 	}
96 	return FALSE;
97 }
98 
99 
100 /* Scan directory searchdir. Input and output parameters come from/go to params. */
filelist_scan_directory_int(const gchar * searchdir,ScanDirParams * params,guint flags)101 static void filelist_scan_directory_int(const gchar *searchdir, ScanDirParams *params, guint flags)
102 {
103 	GDir *dir;
104 	gchar *locale_path = utils_get_locale_from_utf8(searchdir);
105 	gchar *real_path = utils_get_real_path(locale_path);
106 
107 	dir = g_dir_open(locale_path, 0, NULL);
108 	if (!dir || !real_path || g_hash_table_lookup(params->visited_paths, real_path))
109 	{
110 		if (dir != NULL)
111 		{
112 			g_dir_close(dir);
113 		}
114 		g_free(locale_path);
115 		g_free(real_path);
116 		return;
117 	}
118 
119 	g_hash_table_insert(params->visited_paths, real_path, GINT_TO_POINTER(1));
120 
121 	while (TRUE)
122 	{
123 		const gchar *locale_name;
124 		gchar *locale_filename, *utf8_filename, *utf8_name;
125 
126 		locale_name = g_dir_read_name(dir);
127 		if (!locale_name)
128 		{
129 			break;
130 		}
131 
132 		utf8_name = utils_get_utf8_from_locale(locale_name);
133 		locale_filename = g_build_filename(locale_path, locale_name, NULL);
134 		utf8_filename = utils_get_utf8_from_locale(locale_filename);
135 
136 		if (g_file_test(locale_filename, G_FILE_TEST_IS_DIR))
137 		{
138 			if (!filelist_patterns_match(params->ignored_dirs_list, utf8_name))
139 			{
140 				filelist_scan_directory_int(utf8_filename, params, flags);
141 				params->folder_count++;
142 				if (flags & FILELIST_FLAG_ADD_DIRS)
143 				{
144 					params->filelist = g_slist_prepend(params->filelist, g_strdup(utf8_filename));
145 				}
146 			}
147 		}
148 		else if (g_file_test(locale_filename, G_FILE_TEST_IS_REGULAR))
149 		{
150 			if (filelist_patterns_match(params->file_patterns, utf8_name) && !
151 				filelist_patterns_match(params->ignored_file_list, utf8_name))
152 			{
153 				params->file_count++;
154 				params->filelist = g_slist_prepend(params->filelist, g_strdup(utf8_filename));
155 			}
156 		}
157 
158 		g_free(utf8_filename);
159 		g_free(locale_filename);
160 		g_free(utf8_name);
161 	}
162 
163 	g_dir_close(dir);
164 	g_free(locale_path);
165 }
166 
167 
168 /** Scan a directory and return a list of files.
169  *
170  * The function scans directory searchdir and returns a list of files.
171  * The list will only include files which match the patterns in file_patterns.
172  * Directories or files matched by ignored_dirs_patterns or ignored_file_patterns
173  * will not be scanned or added to the list.
174  *
175  * @param files     Can be optionally specified to return the number of matched/found
176  *                  files in *files.
177  * @param folders   Can be optionally specified to return the number of matched/found
178  *                  folders/sub-directories in *folders.
179  * @param searchdir Directory which shall be scanned
180  * @param file_patterns
181  *                  File patterns for matching files (e.g. "*.c") or NULL
182  *                  for all files.
183  * @param ignored_dirs_patterns
184  *                  Patterns for ignored directories
185  * @param ignored_file_patterns
186  *                  Patterns for ignored files
187  * @return GSList of matched files
188  *
189  **/
gp_filelist_scan_directory(guint * files,guint * folders,const gchar * searchdir,gchar ** file_patterns,gchar ** ignored_dirs_patterns,gchar ** ignored_file_patterns)190 GSList *gp_filelist_scan_directory(guint *files, guint *folders, const gchar *searchdir, gchar **file_patterns,
191 		gchar **ignored_dirs_patterns, gchar **ignored_file_patterns)
192 {
193 	ScanDirParams params = { 0 };
194 
195 	if (!file_patterns || !file_patterns[0])
196 	{
197 		const gchar *all_pattern[] = { "*", NULL };
198 		params.file_patterns = filelist_get_precompiled_patterns((gchar **)all_pattern);
199 	}
200 	else
201 	{
202 		params.file_patterns = filelist_get_precompiled_patterns(file_patterns);
203 	}
204 
205 	params.ignored_dirs_list = filelist_get_precompiled_patterns(ignored_dirs_patterns);
206 	params.ignored_file_list = filelist_get_precompiled_patterns(ignored_file_patterns);
207 
208 	params.visited_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
209 	filelist_scan_directory_int(searchdir, &params, 0);
210 	g_hash_table_destroy(params.visited_paths);
211 
212 	g_slist_foreach(params.file_patterns, (GFunc) g_pattern_spec_free, NULL);
213 	g_slist_free(params.file_patterns);
214 
215 	g_slist_foreach(params.ignored_dirs_list, (GFunc) g_pattern_spec_free, NULL);
216 	g_slist_free(params.ignored_dirs_list);
217 
218 	g_slist_foreach(params.ignored_file_list, (GFunc) g_pattern_spec_free, NULL);
219 	g_slist_free(params.ignored_file_list);
220 
221 	if (files != NULL)
222 	{
223 		*files = params.file_count;
224 	}
225 	if (folders != NULL)
226 	{
227 		*folders = params.folder_count;
228 	}
229 
230 	return params.filelist;
231 }
232 
233 
234 /** Scan a directory and return a list of files and directories.
235  *
236  * The function scans directory searchdir and returns a list of files.
237  * The list will only include files which match the patterns in file_patterns.
238  * Directories or files matched by ignored_dirs_patterns or ignored_file_patterns
239  * will not be scanned or added to the list.
240  *
241  * If flags is 0 then the result will be the same as for gp_filelist_scan_directory().
242  *
243  * @param files     Can be optionally specified to return the number of matched/found
244  *                  files in *files.
245  * @param folders   Can be optionally specified to return the number of matched/found
246  *                  folders/sub-directories in *folders.
247  * @param searchdir Directory which shall be scanned
248  * @param file_patterns
249  *                  File patterns for matching files (e.g. "*.c") or NULL
250  *                  for all files.
251  * @param ignored_dirs_patterns
252  *                  Patterns for ignored directories
253  * @param ignored_file_patterns
254  *                  Patterns for ignored files
255  * @param flags     Flags which influence the returned list:
256  *                  - FILELIST_FLAG_ADD_DIRS: if set, directories will be added
257  *                    as own list entries. This also includes empty dirs.
258  * @return GSList of matched files
259  *
260  **/
gp_filelist_scan_directory_full(guint * files,guint * folders,const gchar * searchdir,gchar ** file_patterns,gchar ** ignored_dirs_patterns,gchar ** ignored_file_patterns,guint flags)261 GSList *gp_filelist_scan_directory_full(guint *files, guint *folders, const gchar *searchdir, gchar **file_patterns,
262 		gchar **ignored_dirs_patterns, gchar **ignored_file_patterns, guint flags)
263 {
264 	ScanDirParams params = { 0 };
265 
266 	if (!file_patterns || !file_patterns[0])
267 	{
268 		const gchar *all_pattern[] = { "*", NULL };
269 		params.file_patterns = filelist_get_precompiled_patterns((gchar **)all_pattern);
270 	}
271 	else
272 	{
273 		params.file_patterns = filelist_get_precompiled_patterns(file_patterns);
274 	}
275 
276 	params.ignored_dirs_list = filelist_get_precompiled_patterns(ignored_dirs_patterns);
277 	params.ignored_file_list = filelist_get_precompiled_patterns(ignored_file_patterns);
278 
279 	params.visited_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
280 	filelist_scan_directory_int(searchdir, &params, flags);
281 	g_hash_table_destroy(params.visited_paths);
282 
283 	g_slist_foreach(params.file_patterns, (GFunc) g_pattern_spec_free, NULL);
284 	g_slist_free(params.file_patterns);
285 
286 	g_slist_foreach(params.ignored_dirs_list, (GFunc) g_pattern_spec_free, NULL);
287 	g_slist_free(params.ignored_dirs_list);
288 
289 	g_slist_foreach(params.ignored_file_list, (GFunc) g_pattern_spec_free, NULL);
290 	g_slist_free(params.ignored_file_list);
291 
292 	if (files != NULL)
293 	{
294 		*files = params.file_count;
295 	}
296 	if (folders != NULL)
297 	{
298 		*folders = params.folder_count;
299 	}
300 
301 	return params.filelist;
302 }
303 
304 
305 /** Check if a filepath matches the given patterns.
306  *
307  * If the filepath belongs to a regular file, then TRUE will be returned if
308  * the filepath matches the patterns in file_patterns but does not match the
309  * patterns in ignored_file_patterns.
310  *
311  * If the filepath belongs to a directory, then TRUE will be returned if it
312  * does not match the patterns in ignored_dirs_patterns.
313  *
314  * @param filepath  The file or dorectory path to check
315  * @param file_patterns
316  *                  File patterns for matching files (e.g. "*.c") or NULL
317  *                  for all files.
318  * @param ignored_dirs_patterns
319  *                  Patterns for ignored directories
320  * @param ignored_file_patterns
321  *                  Patterns for ignored files
322  * @return gboolean
323  *
324  **/
gp_filelist_filepath_matches_patterns(const gchar * filepath,gchar ** file_patterns,gchar ** ignored_dirs_patterns,gchar ** ignored_file_patterns)325 gboolean gp_filelist_filepath_matches_patterns(const gchar *filepath, gchar **file_patterns,
326 		gchar **ignored_dirs_patterns, gchar **ignored_file_patterns)
327 {
328 	gboolean match = FALSE;
329 	GSList *file_patterns_list;
330 	GSList *ignored_dirs_list;
331 	GSList *ignored_file_list;
332 
333 	if (!file_patterns || !file_patterns[0])
334 	{
335 		const gchar *all_pattern[] = { "*", NULL };
336 		file_patterns_list = filelist_get_precompiled_patterns((gchar **)all_pattern);
337 	}
338 	else
339 	{
340 		file_patterns_list = filelist_get_precompiled_patterns(file_patterns);
341 	}
342 
343 	ignored_dirs_list = filelist_get_precompiled_patterns(ignored_dirs_patterns);
344 	ignored_file_list = filelist_get_precompiled_patterns(ignored_file_patterns);
345 
346 	if (g_file_test(filepath, G_FILE_TEST_IS_DIR))
347 	{
348 		if (!filelist_patterns_match(ignored_dirs_list, filepath))
349 		{
350 			match = TRUE;
351 		}
352 	}
353 	else if (g_file_test(filepath, G_FILE_TEST_IS_REGULAR))
354 	{
355 		if (filelist_patterns_match(file_patterns_list, filepath) &&
356 			!filelist_patterns_match(ignored_file_list, filepath))
357 		{
358 			match = TRUE;
359 		}
360 	}
361 
362 	g_slist_foreach(file_patterns_list, (GFunc) g_pattern_spec_free, NULL);
363 	g_slist_free(file_patterns_list);
364 
365 	g_slist_foreach(ignored_dirs_list, (GFunc) g_pattern_spec_free, NULL);
366 	g_slist_free(ignored_dirs_list);
367 
368 	g_slist_foreach(ignored_file_list, (GFunc) g_pattern_spec_free, NULL);
369 	g_slist_free(ignored_file_list);
370 
371 	return match;
372 }
373 
374 
375 /* Scan directory searchdir. Let a callback-function decide which files
376    to add and which directories to enter/crawl. */
filelist_scan_directory_callback_int(const gchar * searchdir,ScanDirParamsCallback * params)377 static void filelist_scan_directory_callback_int(const gchar *searchdir, ScanDirParamsCallback *params)
378 {
379 	GDir *dir;
380 	gchar *locale_path = utils_get_locale_from_utf8(searchdir);
381 	gchar *real_path = utils_get_real_path(locale_path);
382 
383 	dir = g_dir_open(locale_path, 0, NULL);
384 	if (!dir || !real_path || g_hash_table_lookup(params->visited_paths, real_path))
385 	{
386 		if (dir != NULL)
387 		{
388 			g_dir_close(dir);
389 		}
390 		g_free(locale_path);
391 		g_free(real_path);
392 		return;
393 	}
394 
395 	g_hash_table_insert(params->visited_paths, real_path, GINT_TO_POINTER(1));
396 
397 	while (TRUE)
398 	{
399 		const gchar *locale_name;
400 		gchar *locale_filename, *utf8_filename, *utf8_name;
401 		gboolean add, enter;
402 
403 		locale_name = g_dir_read_name(dir);
404 		if (!locale_name)
405 		{
406 			break;
407 		}
408 
409 		utf8_name = utils_get_utf8_from_locale(locale_name);
410 		locale_filename = g_build_filename(locale_path, locale_name, NULL);
411 		utf8_filename = utils_get_utf8_from_locale(locale_filename);
412 
413 		/* Call user callback. */
414 		params->callback(locale_filename, &add, &enter, params->userdata);
415 
416 		/* Should the file/path be added to the list? */
417 		if (add)
418 		{
419 			params->filelist = g_slist_prepend(params->filelist, g_strdup(utf8_filename));
420 		}
421 
422 		/* If the path is a directory, should it be entered? */
423 		if (enter && g_file_test(locale_filename, G_FILE_TEST_IS_DIR))
424 		{
425 			filelist_scan_directory_callback_int(utf8_filename, params);
426 		}
427 
428 		g_free(utf8_filename);
429 		g_free(locale_filename);
430 		g_free(utf8_name);
431 	}
432 
433 	g_dir_close(dir);
434 	g_free(locale_path);
435 }
436 
437 
438 /** Scan a directory and return a list of files and directories.
439  *
440  * The function scans directory searchdir and returns a list of files.
441  * The list will only include files for which the callback function returned
442  * 'add == TRUE'. Sub-directories will only be scanned if the callback function
443  * returned 'enter == TRUE'.
444  *
445  * @param searchdir Directory which shall be scanned
446  * @param callback  Function to be called to decide which files to add to
447  *                  the list and to decide which sub-directories to enter
448  *                  or to ignore.
449  * @param userdata  A pointer which is transparently passed through
450  *                  to the callback function.
451  * @return GSList of matched files
452  *
453  **/
gp_filelist_scan_directory_callback(const gchar * searchdir,void (* callback)(const gchar * path,gboolean * add,gboolean * enter,void * userdata),void * userdata)454 GSList *gp_filelist_scan_directory_callback(const gchar *searchdir,
455 	void (*callback)(const gchar *path, gboolean *add, gboolean *enter, void *userdata),
456 	void *userdata)
457 {
458 	ScanDirParamsCallback params = { 0 };
459 
460 	params.callback = callback;
461 	params.userdata = userdata;
462 	params.visited_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
463 	filelist_scan_directory_callback_int(searchdir, &params);
464 	g_hash_table_destroy(params.visited_paths);
465 
466 	return params.filelist;
467 }
468