OSDN Git Service

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