OSDN Git Service

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