OSDN Git Service

Merge from rev.7128:7151
[winmerge-jp/winmerge-jp.git] / Src / FileFilterMgr.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    License (GPLv2+):
3 //    This program is free software; you can redistribute it and/or modify it
4 //    under the terms of the GNU General Public License as published by the
5 //    Free Software Foundation; either version 2 of the License, or (at your
6 //    option) any later version.
7 //    This program is distributed in the hope that it will be useful, but
8 //    WITHOUT ANY WARRANTY; without even the implied warranty of
9 //    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10 //    General Public License for more details.
11 //    You should have received a copy of the GNU General Public License
12 //    along with this program; if not, write to the Free Software
13 //    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
14 /////////////////////////////////////////////////////////////////////////////
15 /**
16  *  @file FileFilterMgr.cpp
17  *
18  *  @brief Implementation of FileFilterMgr and supporting routines
19  */ 
20 // ID line follows -- this is updated by SVN
21 // $Id: FileFilterMgr.cpp 7024 2009-10-22 18:26:45Z kimmov $
22
23 #include <windows.h>
24 #include <string.h>
25 #include <vector>
26 #include <tchar.h>
27 #include "UnicodeString.h"
28 #include "FileFilter.h"
29 #include "pcre.h"
30 #include "FileFilterMgr.h"
31 #include "UniFile.h"
32 #include "coretools.h"
33 #include "Ucs2Utf8.h"
34 #include "paths.h"
35
36 using std::vector;
37
38 static void AddFilterPattern(vector<FileFilterElement*> *filterList, String & str);
39
40 /**
41  * @brief Destructor, frees all filters.
42  */
43 FileFilterMgr::~FileFilterMgr()
44 {
45         DeleteAllFilters();
46 }
47
48 /**
49  * @brief Loads filterfile from disk and adds it to filters.
50  * @param [in] szFilterFile Filter file to load.
51  * @return FILTER_OK if succeeded or one of FILTER_RETVALUE values on error.
52  */
53 int FileFilterMgr::AddFilter(LPCTSTR szFilterFile)
54 {
55         int errorcode = FILTER_OK;
56         FileFilter * pFilter = LoadFilterFile(szFilterFile, errorcode);
57         if (pFilter)
58                 m_filters.push_back(pFilter);
59         return errorcode;
60 }
61
62 /**
63  * @brief Load all filter files matching pattern from disk into internal filter set.
64  * @param [in] dir Directory from where filters are loaded.
65  * @param [in] szPattern Pattern for filters to load filters, for example "*.flt".
66  * @param [in] szExt File-extension of filter files.
67  */
68 void FileFilterMgr::LoadFromDirectory(LPCTSTR dir, LPCTSTR szPattern, LPCTSTR szExt)
69 {
70         const int extlen = szExt ? _tcslen(szExt) : 0;
71         const String pattern = paths_ConcatPath(dir, szPattern);
72         WIN32_FIND_DATA ff;
73         HANDLE h = FindFirstFile(pattern.c_str(), &ff);
74         if (h != INVALID_HANDLE_VALUE)
75         {
76                 do
77                 {
78                         if (ff.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
79                                 continue;
80                         String filename = ff.cFileName;
81                         if (szExt)
82                         {
83                                 // caller specified a specific extension
84                                 // (This is really a workaround for brokenness in windows, which
85                                 //  doesn't screen correctly on extension in pattern)
86                                 String ext = filename.substr(filename.length() - extlen);
87                                 if (string_compare_nocase(szExt, ext) != 0)
88                                         return;
89                         }
90
91                         String filterpath = paths_ConcatPath(dir, ff.cFileName);
92                         AddFilter(filterpath.c_str());
93                 } while (FindNextFile(h, &ff));
94                 FindClose(h);
95         }
96 }
97
98 /**
99  * @brief Removes filter from filterlist.
100  *
101  * @param [in] szFilterFile Filename of filter to remove.
102  */
103 void FileFilterMgr::RemoveFilter(LPCTSTR szFilterFile)
104 {
105         // Note that m_filters.GetSize can change during loop
106         vector<FileFilter*>::iterator iter = m_filters.begin();
107         while (iter != m_filters.end())
108         {
109                 if (string_compare_nocase((*iter)->fullpath, szFilterFile) == 0)
110                 {
111                         delete (*iter);
112                         m_filters.erase(iter);
113                         break;
114                 }
115                 ++iter;
116         }
117 }
118
119 /**
120  * @brief Removes all filters from current list.
121  */
122 void FileFilterMgr::DeleteAllFilters()
123 {
124         while (!m_filters.empty())
125         {
126                 FileFilter* filter = m_filters.back();
127                 delete filter;
128                 m_filters.pop_back();
129         }
130 }
131
132 /**
133  * @brief Add a single pattern (if nonempty & valid) to a pattern list.
134  *
135  * @param [in] filterList List where pattern is added.
136  * @param [in] str Temporary variable (ie, it may be altered)
137  */
138 static void AddFilterPattern(vector<FileFilterElement*> *filterList, String & str)
139 {
140         LPCTSTR commentLeader = _T("##"); // Starts comment
141         str = string_trim_ws_begin(str);
142
143         // Ignore lines beginning with '##'
144         size_t pos = str.find(commentLeader);
145         if (pos == 0)
146                 return;
147
148         // Find possible comment-separator '<whitespace>##'
149         while (pos != std::string::npos && !_istspace(str[pos - 1]))
150                 pos = str.find(commentLeader, pos + 1);
151
152         // Remove comment and whitespaces before it
153         if (pos != std::string::npos)
154                 str = str.substr(0, pos);
155         str = string_trim_ws_end(str);
156         if (str.empty())
157                 return;
158
159         const char * errormsg = NULL;
160         int erroroffset = 0;
161         int  regexStringBufLen = _tcslen(str.c_str()) * sizeof(TCHAR) * 3 + 1;
162         char *regexString = (char *)malloc(regexStringBufLen);
163         int regexLen = 0;
164         int pcre_opts = 0;
165
166 #ifdef UNICODE
167         // For unicode builds, use UTF-8.
168         // Convert pattern to UTF-8 and set option for PCRE to specify UTF-8.
169         regexLen = TransformUcs2ToUtf8(str.c_str(), str.length(),
170                 regexString, regexStringBufLen);
171         regexString[regexLen] = '\0';
172         pcre_opts |= PCRE_UTF8;
173 #else
174         strcpy(regexString, (LPCTSTR)str.c_str());
175         regexLen = strlen(regexString);
176 #endif
177         pcre_opts |= PCRE_CASELESS;
178         
179         pcre *regexp = pcre_compile(regexString, pcre_opts, &errormsg,
180                 &erroroffset, NULL);
181         if (regexp)
182         {
183                 FileFilterElement *elem = new FileFilterElement();
184                 errormsg = NULL;
185
186                 pcre_extra *pe = pcre_study(regexp, 0, &errormsg);
187                 elem->pRegExp = regexp;
188                 
189                 if (pe != NULL && errormsg != NULL)
190                         elem->pRegExpExtra = pe;
191                 
192                 filterList->push_back(elem);
193         }
194
195         free(regexString);
196 }
197
198 /**
199  * @brief Parse a filter file, and add it to array if valid.
200  *
201  * @param [in] szFilePath Path (w/ filename) to file to load.
202  * @param [out] error Error-code if loading failed (returned NULL).
203  * @return Pointer to new filter, or NULL if error (check error code too).
204  */
205 FileFilter * FileFilterMgr::LoadFilterFile(LPCTSTR szFilepath, int & error)
206 {
207         UniMemFile file;
208         if (!file.OpenReadOnly(szFilepath))
209         {
210                 error = FILTER_ERROR_FILEACCESS;
211                 return NULL;
212         }
213
214         file.ReadBom(); // in case it is a Unicode file, let UniMemFile handle BOM
215
216         String fileName;
217         SplitFilename(szFilepath, NULL, &fileName, NULL);
218         FileFilter *pfilter = new FileFilter;
219         pfilter->fullpath = szFilepath;
220         pfilter->name = fileName.c_str(); // Filename is the default name
221
222         String sLine;
223         bool lossy = false;
224         bool bLinesLeft = true;
225         do
226         {
227                 // Returns false when last line is read
228                 String tmpLine;
229                 bLinesLeft = file.ReadString(tmpLine, &lossy);
230                 sLine = tmpLine;
231                 sLine = string_trim_ws(sLine);
232
233                 if (0 == _tcsncmp(sLine.c_str(), _T("name:"), 5))
234                 {
235                         // specifies display name
236                         String str = sLine.substr(5);
237                         str = string_trim_ws_begin(str);
238                         if (!str.empty())
239                                 pfilter->name = str;
240                 }
241                 else if (0 == _tcsncmp(sLine.c_str(), _T("desc:"), 5))
242                 {
243                         // specifies display name
244                         String str = sLine.substr(5);
245                         str = string_trim_ws_begin(str);
246                         if (!str.empty())
247                                 pfilter->description = str;
248                 }
249                 else if (0 == _tcsncmp(sLine.c_str(), _T("def:"), 4))
250                 {
251                         // specifies default
252                         String str = sLine.substr(4);
253                         str = string_trim_ws_begin(str);
254                         if (str == _T("0") || str == _T("no") || str == _T("exclude"))
255                                 pfilter->default_include = false;
256                         else if (str == _T("1") || str == _T("yes") || str == _T("include"))
257                                 pfilter->default_include = true;
258                 }
259                 else if (0 == _tcsncmp(sLine.c_str(), _T("f:"), 2))
260                 {
261                         // file filter
262                         String str = sLine.substr(2);
263                         AddFilterPattern(&pfilter->filefilters, str);
264                 }
265                 else if (0 == _tcsncmp(sLine.c_str(), _T("d:"), 2))
266                 {
267                         // directory filter
268                         String str = sLine.substr(2);
269                         AddFilterPattern(&pfilter->dirfilters, str);
270                 }
271         } while (bLinesLeft);
272
273         return pfilter;
274 }
275
276 /**
277  * @brief Give client back a pointer to the actual filter.
278  *
279  * @param [in] szFilterPath Full path to filterfile.
280  * @return Pointer to found filefilter or NULL;
281  * @note We just do a linear search, because this is seldom called
282  */
283 FileFilter * FileFilterMgr::GetFilterByPath(LPCTSTR szFilterPath)
284 {
285         vector<FileFilter*>::const_iterator iter = m_filters.begin();
286         while (iter != m_filters.end())
287         {
288                 if (string_compare_nocase((*iter)->fullpath, szFilterPath) == 0)
289                         return (*iter);
290                 ++iter;
291         }
292         return 0;
293 }
294
295 /**
296  * @brief Test given string against given regexp list.
297  *
298  * @param [in] filterList List of regexps to test against.
299  * @param [in] szTest String to test against regexps.
300  * @return TRUE if string passes
301  * @note Matching stops when first match is found.
302  */
303 BOOL TestAgainstRegList(const vector<FileFilterElement*> *filterList, LPCTSTR szTest)
304 {
305         vector<FileFilterElement*>::const_iterator iter = filterList->begin();
306         while (iter != filterList->end())
307         {
308                 //const FileFilterElement & elem = filterList.GetNext(pos);
309                 int ovector[30];
310                 int compStringBufLen = _tcslen(szTest) * sizeof(TCHAR) * 3 + 1;
311                 char *compString = (char *)malloc(compStringBufLen);
312                 int stringLen = 0;
313                 TCHAR * tempName = _tcsdup(szTest); // Create temp copy for conversions
314                 TCHAR * cmpStr = _tcsupr(tempName);
315
316 #ifdef UNICODE
317                 stringLen = TransformUcs2ToUtf8(cmpStr, _tcslen(cmpStr),
318                         compString, compStringBufLen);
319 #else
320                 strcpy(compString, cmpStr);
321                 stringLen = strlen(compString);
322 #endif
323
324                 pcre * regexp = (*iter)->pRegExp;
325                 pcre_extra * extra = (*iter)->pRegExpExtra;
326                 int result = pcre_exec(regexp, extra, compString, stringLen,
327                         0, 0, ovector, 30);
328
329                 free(tempName);
330                 free(compString);
331
332                 if (result >= 0)
333                         return TRUE;
334
335                 ++iter;
336         }
337         return FALSE;
338 }
339
340 /**
341  * @brief Test given filename against filefilter.
342  *
343  * Test filename against active filefilter. If matching rule is found
344  * we must first determine type of rule that matched. If we return FALSE
345  * from this function directory scan marks file as skipped.
346  *
347  * @param [in] pFilter Pointer to filefilter
348  * @param [in] szFileName Filename to test
349  * @return TRUE if file passes the filter
350  */
351 BOOL FileFilterMgr::TestFileNameAgainstFilter(const FileFilter * pFilter,
352         LPCTSTR szFileName) const
353 {
354         if (!pFilter)
355                 return TRUE;
356         if (TestAgainstRegList(&pFilter->filefilters, szFileName))
357                 return !pFilter->default_include;
358         return pFilter->default_include;
359 }
360
361 /**
362  * @brief Test given directory name against filefilter.
363  *
364  * Test directory name against active filefilter. If matching rule is found
365  * we must first determine type of rule that matched. If we return FALSE
366  * from this function directory scan marks file as skipped.
367  *
368  * @param [in] pFilter Pointer to filefilter
369  * @param [in] szDirName Directory name to test
370  * @return TRUE if directory name passes the filter
371  */
372 BOOL FileFilterMgr::TestDirNameAgainstFilter(const FileFilter * pFilter,
373         LPCTSTR szDirName) const
374 {
375         if (!pFilter)
376                 return TRUE;
377         if (TestAgainstRegList(&pFilter->dirfilters, szDirName))
378                 return !pFilter->default_include;
379         return pFilter->default_include;
380 }
381
382 /**
383  * @brief Return name of filter.
384  *
385  * @param [in] i Index of filter.
386  * @return Name of filter in given index.
387  */
388 String FileFilterMgr::GetFilterName(int i) const
389 {
390         return m_filters[i]->name; 
391 }
392
393 /**
394  * @brief Return name of filter.
395  * @param [in] pFilter Filter to get name for.
396  * @return Given filter's name.
397  */
398 String FileFilterMgr::GetFilterName(const FileFilter *pFilter) const
399 {
400         return pFilter->name; 
401 }
402
403 /**
404  * @brief Return description of filter.
405  *
406  * @param [in] i Index of filter.
407  * @return Description of filter in given index.
408  */
409 String FileFilterMgr::GetFilterDesc(int i) const
410 {
411         return m_filters[i]->description; 
412 }
413
414 /**
415  * @brief Return description of filter.
416  * @param [in] pFilter Filter to get description for.
417  * @return Given filter's description.
418  */
419 String FileFilterMgr::GetFilterDesc(const FileFilter *pFilter) const
420 {
421         return pFilter->description;
422 }
423
424 /**
425  * @brief Return full path to filter.
426  *
427  * @param [in] i Index of filter.
428  * @return Full path of filter in given index.
429  */
430 String FileFilterMgr::GetFilterPath(int i) const
431 {
432         return m_filters[i]->fullpath;
433 }
434
435 /**
436  * @brief Return full path to filter.
437  *
438  * @param [in] pFilter Pointer to filter.
439  * @return Full path of filter.
440  */
441 String FileFilterMgr::GetFullpath(FileFilter * pfilter) const
442 {
443         return pfilter->fullpath;
444 }
445
446 /**
447  * @brief Reload filter from disk
448  *
449  * Reloads filter from disk. This is done by creating a new one
450  * to substitute for old one.
451  * @param [in] pFilter Pointer to filter to reload.
452  * @return FILTER_OK when succeeds, one of FILTER_RETVALUE values on error.
453  * @note Given filter (pfilter) is freed and must not be used anymore.
454  * @todo Should return new filter.
455  */
456 int FileFilterMgr::ReloadFilterFromDisk(FileFilter * pfilter)
457 {
458         int errorcode = FILTER_OK;
459         FileFilter * newfilter = LoadFilterFile(pfilter->fullpath.c_str(), errorcode);
460
461         if (newfilter == NULL)
462         {
463                 return errorcode;
464         }
465
466         vector<FileFilter*>::iterator iter = m_filters.begin();
467         while (iter != m_filters.end())
468         {
469                 if (pfilter == (*iter))
470                 {
471                         delete (*iter);
472                         m_filters.erase(iter);
473                         break;
474                 }
475         }
476         m_filters.push_back(newfilter);
477         return errorcode;
478 }
479
480 /**
481  * @brief Reload filter from disk.
482  *
483  * Reloads filter from disk. This is done by creating a new one
484  * to substitute for old one.
485  * @param [in] szFullPath Full path to filter file to reload.
486  * @return FILTER_OK when succeeds or one of FILTER_RETVALUE values when fails.
487  */
488 int FileFilterMgr::ReloadFilterFromDisk(LPCTSTR szFullPath)
489 {
490         int errorcode = FILTER_OK;
491         FileFilter * filter = GetFilterByPath(szFullPath);
492         if (filter)
493                 errorcode = ReloadFilterFromDisk(filter);
494         else
495                 errorcode = FILTER_NOTFOUND;
496         return errorcode;
497 }