OSDN Git Service

Fix bitbucket issue #155: Slow startup with documents folder redirected to high-laten...
[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
21 #include "pch.h"
22 #include "FileFilterMgr.h"
23 #include <vector>
24 #include <Poco/String.h>
25 #include <Poco/Glob.h>
26 #include <Poco/RegularExpression.h>
27 #include "DirTravel.h"
28 #include "DirItem.h"
29 #include "UnicodeString.h"
30 #include "FileFilter.h"
31 #include "UniFile.h"
32 #include "paths.h"
33
34 using std::vector;
35 using Poco::Glob;
36 using Poco::icompare;
37 using Poco::RegularExpression;
38
39 static void AddFilterPattern(vector<FileFilterElementPtr> *filterList, String & str);
40
41 /**
42  * @brief Destructor, frees all filters.
43  */
44 FileFilterMgr::~FileFilterMgr()
45 {
46         DeleteAllFilters();
47 }
48
49 /**
50  * @brief Loads filterfile from disk and adds it to filters.
51  * @param [in] szFilterFile Filter file to load.
52  * @return FILTER_OK if succeeded or one of FILTER_RETVALUE values on error.
53  */
54 int FileFilterMgr::AddFilter(const String& szFilterFile)
55 {
56         int errorcode = FILTER_OK;
57         FileFilter * pFilter = LoadFilterFile(szFilterFile, errorcode);
58         if (pFilter != nullptr)
59                 m_filters.push_back(FileFilterPtr(pFilter));
60         return errorcode;
61 }
62
63 /**
64  * @brief Load all filter files matching pattern from disk into internal filter set.
65  * @param [in] dir Directory from where filters are loaded.
66  * @param [in] szPattern Pattern for filters to load filters, for example "*.flt".
67  * @param [in] szExt File-extension of filter files.
68  */
69 void FileFilterMgr::LoadFromDirectory(const String& dir, const String& szPattern, const String& szExt)
70 {
71         try
72         {
73                 DirItemArray dirs, files;
74                 LoadAndSortFiles(dir, &dirs, &files, false);
75                 Glob glb(ucr::toUTF8(szPattern));
76         
77                 for (DirItem& item: files)
78                 {
79                         String filename = item.filename;
80                         if (!glb.match(ucr::toUTF8(filename)))
81                                 continue;
82                         if (!szExt.empty())
83                         {
84                                 // caller specified a specific extension
85                                 // (This is really a workaround for brokenness in windows, which
86                                 //  doesn't screen correctly on extension in pattern)
87                                 const String ext = filename.substr(filename.length() - szExt.length());
88                                 if (strutils::compare_nocase(szExt, ext) != 0)
89                                         return;
90                         }
91
92                         String filterpath = paths::ConcatPath(dir, filename);
93                         AddFilter(filterpath);
94                 }
95         }
96         catch (...)
97         {
98         }
99 }
100
101 /**
102  * @brief Removes filter from filterlist.
103  *
104  * @param [in] szFilterFile Filename of filter to remove.
105  */
106 void FileFilterMgr::RemoveFilter(const String& szFilterFile)
107 {
108         // Note that m_filters.GetSize can change during loop
109         vector<FileFilterPtr>::iterator iter = m_filters.begin();
110         while (iter != m_filters.end())
111         {
112                 if (strutils::compare_nocase((*iter)->fullpath, szFilterFile) == 0)
113                 {
114                         m_filters.erase(iter);
115                         break;
116                 }
117                 ++iter;
118         }
119 }
120
121 /**
122  * @brief Removes all filters from current list.
123  */
124 void FileFilterMgr::DeleteAllFilters()
125 {
126         m_filters.clear();
127 }
128
129 /**
130  * @brief Add a single pattern (if nonempty & valid) to a pattern list.
131  *
132  * @param [in] filterList List where pattern is added.
133  * @param [in] str Temporary variable (ie, it may be altered)
134  */
135 static void AddFilterPattern(vector<FileFilterElementPtr> *filterList, String & str)
136 {
137         const String& commentLeader = _T("##"); // Starts comment
138         str = strutils::trim_ws_begin(str);
139
140         // Ignore lines beginning with '##'
141         size_t pos = str.find(commentLeader);
142         if (pos == 0)
143                 return;
144
145         // Find possible comment-separator '<whitespace>##'
146         while (pos != std::string::npos && !(str[pos - 1] == ' ' || str[pos - 1] == '\t'))
147                 pos = str.find(commentLeader, pos + 1);
148
149         // Remove comment and whitespaces before it
150         if (pos != std::string::npos)
151                 str = str.substr(0, pos);
152         str = strutils::trim_ws_end(str);
153         if (str.empty())
154                 return;
155
156         int re_opts = RegularExpression::RE_CASELESS;
157         std::string regexString = ucr::toUTF8(str);
158         re_opts |= RegularExpression::RE_UTF8;
159         try
160         {
161                 filterList->push_back(FileFilterElementPtr(new FileFilterElement(regexString, re_opts)));
162         }
163         catch (...)
164         {
165                 // TODO:
166         }
167 }
168
169 /**
170  * @brief Parse a filter file, and add it to array if valid.
171  *
172  * @param [in] szFilePath Path (w/ filename) to file to load.
173  * @param [out] error Error-code if loading failed (returned `nullptr`).
174  * @return Pointer to new filter, or `nullptr` if error (check error code too).
175  */
176 FileFilter * FileFilterMgr::LoadFilterFile(const String& szFilepath, int & error)
177 {
178         UniMemFile file;
179         if (!file.OpenReadOnly(szFilepath))
180         {
181                 error = FILTER_ERROR_FILEACCESS;
182                 return nullptr;
183         }
184
185         file.ReadBom(); // in case it is a Unicode file, let UniMemFile handle BOM
186
187         String fileName;
188         paths::SplitFilename(szFilepath, nullptr, &fileName, nullptr);
189         FileFilter *pfilter = new FileFilter;
190         pfilter->fullpath = szFilepath;
191         pfilter->name = fileName; // Filename is the default name
192
193         String sLine;
194         bool lossy = false;
195         bool bLinesLeft = true;
196         do
197         {
198                 // Returns false when last line is read
199                 String tmpLine;
200                 bLinesLeft = file.ReadString(tmpLine, &lossy);
201                 sLine = tmpLine;
202                 sLine = strutils::trim_ws(sLine);
203
204                 if (0 == sLine.compare(0, 5, _T("name:"), 5))
205                 {
206                         // specifies display name
207                         String str = sLine.substr(5);
208                         str = strutils::trim_ws_begin(str);
209                         if (!str.empty())
210                                 pfilter->name = str;
211                 }
212                 else if (0 == sLine.compare(0, 5, _T("desc:"), 5))
213                 {
214                         // specifies display name
215                         String str = sLine.substr(5);
216                         str = strutils::trim_ws_begin(str);
217                         if (!str.empty())
218                                 pfilter->description = str;
219                 }
220                 else if (0 == sLine.compare(0, 4, _T("def:"), 4))
221                 {
222                         // specifies default
223                         String str = sLine.substr(4);
224                         str = strutils::trim_ws_begin(str);
225                         if (str == _T("0") || str == _T("no") || str == _T("exclude"))
226                                 pfilter->default_include = false;
227                         else if (str == _T("1") || str == _T("yes") || str == _T("include"))
228                                 pfilter->default_include = true;
229                 }
230                 else if (0 == sLine.compare(0, 2, _T("f:"), 2))
231                 {
232                         // file filter
233                         String str = sLine.substr(2);
234                         AddFilterPattern(&pfilter->filefilters, str);
235                 }
236                 else if (0 == sLine.compare(0, 2, _T("d:"), 2))
237                 {
238                         // directory filter
239                         String str = sLine.substr(2);
240                         AddFilterPattern(&pfilter->dirfilters, str);
241                 }
242         } while (bLinesLeft);
243
244         return pfilter;
245 }
246
247 /**
248  * @brief Give client back a pointer to the actual filter.
249  *
250  * @param [in] szFilterPath Full path to filterfile.
251  * @return Pointer to found filefilter or `nullptr`;
252  * @note We just do a linear search, because this is seldom called
253  */
254 FileFilter * FileFilterMgr::GetFilterByPath(const String& szFilterPath)
255 {
256         vector<FileFilterPtr>::const_iterator iter = m_filters.begin();
257         while (iter != m_filters.end())
258         {
259                 if (strutils::compare_nocase((*iter)->fullpath, szFilterPath) == 0)
260                         return (*iter).get();
261                 ++iter;
262         }
263         return 0;
264 }
265
266 /**
267  * @brief Test given string against given regexp list.
268  *
269  * @param [in] filterList List of regexps to test against.
270  * @param [in] szTest String to test against regexps.
271  * @return true if string passes
272  * @note Matching stops when first match is found.
273  */
274 bool TestAgainstRegList(const vector<FileFilterElementPtr> *filterList, const String& szTest)
275 {
276         if (filterList->size() == 0)
277                 return false;
278
279         std::string compString;
280         ucr::toUTF8(szTest, compString);
281         vector<FileFilterElementPtr>::const_iterator iter = filterList->begin();
282         while (iter != filterList->end())
283         {
284                 RegularExpression::Match match;
285                 try
286                 {
287                         if ((*iter)->regexp.match(compString, 0, match) > 0)
288                                 return true;
289                 }
290                 catch (...)
291                 {
292                         // TODO:
293                 }
294                 
295                 ++iter;
296         }
297         return false;
298 }
299
300 /**
301  * @brief Test given filename against filefilter.
302  *
303  * Test filename against active filefilter. If matching rule is found
304  * we must first determine type of rule that matched. If we return false
305  * from this function directory scan marks file as skipped.
306  *
307  * @param [in] pFilter Pointer to filefilter
308  * @param [in] szFileName Filename to test
309  * @return true if file passes the filter
310  */
311 bool FileFilterMgr::TestFileNameAgainstFilter(const FileFilter * pFilter,
312         const String& szFileName) const
313 {
314         if (pFilter == nullptr)
315                 return true;
316         if (TestAgainstRegList(&pFilter->filefilters, szFileName))
317                 return !pFilter->default_include;
318         return pFilter->default_include;
319 }
320
321 /**
322  * @brief Test given directory name against filefilter.
323  *
324  * Test directory name against active filefilter. If matching rule is found
325  * we must first determine type of rule that matched. If we return false
326  * from this function directory scan marks file as skipped.
327  *
328  * @param [in] pFilter Pointer to filefilter
329  * @param [in] szDirName Directory name to test
330  * @return true if directory name passes the filter
331  */
332 bool FileFilterMgr::TestDirNameAgainstFilter(const FileFilter * pFilter,
333         const String& szDirName) const
334 {
335         if (pFilter == nullptr)
336                 return true;
337         if (TestAgainstRegList(&pFilter->dirfilters, szDirName))
338                 return !pFilter->default_include;
339         return pFilter->default_include;
340 }
341
342 /**
343  * @brief Return name of filter.
344  *
345  * @param [in] i Index of filter.
346  * @return Name of filter in given index.
347  */
348 String FileFilterMgr::GetFilterName(int i) const
349 {
350         return m_filters[i]->name; 
351 }
352
353 /**
354  * @brief Return name of filter.
355  * @param [in] pFilter Filter to get name for.
356  * @return Given filter's name.
357  */
358 String FileFilterMgr::GetFilterName(const FileFilter *pFilter) const
359 {
360         return pFilter->name; 
361 }
362
363 /**
364  * @brief Return description of filter.
365  *
366  * @param [in] i Index of filter.
367  * @return Description of filter in given index.
368  */
369 String FileFilterMgr::GetFilterDesc(int i) const
370 {
371         return m_filters[i]->description; 
372 }
373
374 /**
375  * @brief Return description of filter.
376  * @param [in] pFilter Filter to get description for.
377  * @return Given filter's description.
378  */
379 String FileFilterMgr::GetFilterDesc(const FileFilter *pFilter) const
380 {
381         return pFilter->description;
382 }
383
384 /**
385  * @brief Return full path to filter.
386  *
387  * @param [in] i Index of filter.
388  * @return Full path of filter in given index.
389  */
390 String FileFilterMgr::GetFilterPath(int i) const
391 {
392         return m_filters[i]->fullpath;
393 }
394
395 /**
396  * @brief Return full path to filter.
397  *
398  * @param [in] pFilter Pointer to filter.
399  * @return Full path of filter.
400  */
401 String FileFilterMgr::GetFullpath(FileFilter * pfilter) const
402 {
403         return pfilter->fullpath;
404 }
405
406 /**
407  * @brief Reload filter from disk
408  *
409  * Reloads filter from disk. This is done by creating a new one
410  * to substitute for old one.
411  * @param [in] pFilter Pointer to filter to reload.
412  * @return FILTER_OK when succeeds, one of FILTER_RETVALUE values on error.
413  * @note Given filter (pfilter) is freed and must not be used anymore.
414  * @todo Should return new filter.
415  */
416 int FileFilterMgr::ReloadFilterFromDisk(FileFilter * pfilter)
417 {
418         int errorcode = FILTER_OK;
419         FileFilter * newfilter = LoadFilterFile(pfilter->fullpath, errorcode);
420
421         if (newfilter == nullptr)
422         {
423                 return errorcode;
424         }
425
426         vector<FileFilterPtr>::iterator iter = m_filters.begin();
427         while (iter != m_filters.end())
428         {
429                 if (pfilter == (*iter).get())
430                 {
431                         m_filters.erase(iter);
432                         break;
433                 }
434         }
435         m_filters.push_back(FileFilterPtr(newfilter));
436         return errorcode;
437 }
438
439 /**
440  * @brief Reload filter from disk.
441  *
442  * Reloads filter from disk. This is done by creating a new one
443  * to substitute for old one.
444  * @param [in] szFullPath Full path to filter file to reload.
445  * @return FILTER_OK when succeeds or one of FILTER_RETVALUE values when fails.
446  */
447 int FileFilterMgr::ReloadFilterFromDisk(const String& szFullPath)
448 {
449         int errorcode = FILTER_OK;
450         FileFilter * filter = GetFilterByPath(szFullPath);
451         if (filter)
452                 errorcode = ReloadFilterFromDisk(filter);
453         else
454                 errorcode = FILTER_NOTFOUND;
455         return errorcode;
456 }