OSDN Git Service

Avoid an assertion failure when loading settings from winmerge.ini
[winmerge-jp/winmerge-jp.git] / Src / FileFilterHelper.cpp
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /** 
3  * @file  FileFilterHelper.cpp
4  *
5  * @brief Implementation file for FileFilterHelper class
6  */
7
8 #include "pch.h"
9 #include "FileFilterHelper.h"
10 #include "UnicodeString.h"
11 #include "FilterList.h"
12 #include "DirItem.h"
13 #include "FileFilterMgr.h"
14 #include "paths.h"
15 #include "Environment.h"
16 #include "unicoder.h"
17
18 using std::vector;
19
20 /** 
21  * @brief Constructor, creates new filtermanager.
22  */
23 FileFilterHelper::FileFilterHelper()
24 : m_pMaskFilter(nullptr)
25 , m_bUseMask(true)
26 , m_fileFilterMgr(new FileFilterMgr)
27 , m_currentFilter(nullptr)
28 {
29 }
30
31 /** 
32  * @brief Destructor, deletes filtermanager.
33  */
34 FileFilterHelper::~FileFilterHelper()
35 {
36 }
37
38 /**
39  * @brief Store current filter path.
40  *
41  * Select filter based on filepath. If filter with that path
42  * is found select it. Otherwise set path to empty (default).
43  * @param [in] szFileFilterPath Full path to filter to select.
44  */
45 void FileFilterHelper::SetFileFilterPath(const String& szFileFilterPath)
46 {
47         // Use none as default path
48         m_sFileFilterPath.clear();
49
50         if (m_fileFilterMgr == nullptr)
51                 return;
52
53         // Don't bother to lookup empty path
54         if (!szFileFilterPath.empty())
55         {
56                 m_currentFilter = m_fileFilterMgr->GetFilterByPath(szFileFilterPath);
57                 if (m_currentFilter != nullptr)
58                         m_sFileFilterPath = szFileFilterPath;
59         }
60 }
61
62 /**
63  * @brief Get list of filters currently available.
64  *
65  * @param [out] selected Filepath of currently selected filter.
66  * @return Filter list to receive found filters.
67  */
68 std::vector<FileFilterInfo> FileFilterHelper::GetFileFilters(String & selected) const
69 {
70         std::vector<FileFilterInfo> filters;
71         if (m_fileFilterMgr != nullptr)
72         {
73                 const int count = m_fileFilterMgr->GetFilterCount();
74                 filters.reserve(count);
75                 for (int i = 0; i < count; ++i)
76                 {
77                         FileFilterInfo filter;
78                         filter.fullpath = m_fileFilterMgr->GetFilterPath(i);
79                         filter.name = m_fileFilterMgr->GetFilterName(i);
80                         filter.description = m_fileFilterMgr->GetFilterDesc(i);
81                         filters.push_back(filter);
82                 }
83         }
84         selected = m_sFileFilterPath;
85         return filters;
86 }
87
88 /**
89  * @brief Return name of filter in given file.
90  * If no filter cannot be found, return empty string.
91  * @param [in] filterPath Path to filterfile.
92  * @sa FileFilterHelper::GetFileFilterPath()
93  */
94 String FileFilterHelper::GetFileFilterName(const String& filterPath) const
95 {
96         String selected;
97         String name;
98         vector<FileFilterInfo> filters = GetFileFilters(selected);
99         vector<FileFilterInfo>::const_iterator iter = filters.begin();
100         while (iter != filters.end())
101         {
102                 if ((*iter).fullpath == filterPath)
103                 {
104                         name = (*iter).name;
105                         break;
106                 }
107                 ++iter;
108         }
109         return name;
110 }
111
112 /** 
113  * @brief Return path to filter with given name.
114  * @param [in] filterName Name of filter.
115  * @sa FileFilterHelper::GetFileFilterName()
116  */
117 String FileFilterHelper::GetFileFilterPath(const String& filterName) const
118 {
119         String selected;
120         String path;
121         vector<FileFilterInfo> filters = GetFileFilters(selected);
122         vector<FileFilterInfo>::const_iterator iter = filters.begin();
123         while (iter != filters.end())
124         {
125                 if ((*iter).name == filterName)
126                 {
127                         path = (*iter).fullpath;
128                         break;
129                 }
130                 ++iter;
131         }
132         return path;
133 }
134
135 /** 
136  * @brief Set User's filter folder.
137  * @param [in] filterPath Location of User's filters.
138  */
139 void FileFilterHelper::SetUserFilterPath(const String & filterPath)
140 {
141         m_sUserSelFilterPath = filterPath;
142         paths::normalize(m_sUserSelFilterPath);
143 }
144
145 /** 
146  * @brief Select between mask and filterfile.
147  * @param [in] bUseMask If true we use mask instead of filter files.
148  */
149 void FileFilterHelper::UseMask(bool bUseMask)
150 {
151         m_bUseMask = bUseMask;
152         if (m_bUseMask)
153         {
154                 if (m_pMaskFilter == nullptr)
155                 {
156                         m_pMaskFilter.reset(new FilterList);
157                 }
158         }
159         else
160         {
161                 m_pMaskFilter.reset();
162         }
163 }
164
165 /** 
166  * @brief Set filemask for filtering.
167  * @param [in] strMask Mask to set (e.g. *.cpp;*.h).
168  */
169 void FileFilterHelper::SetMask(const String& strMask)
170 {
171         if (!m_bUseMask)
172         {
173                 throw "Filter mask tried to set when masks disabled!";
174         }
175         m_sMask = strMask;
176         String regExp = ParseExtensions(strMask);
177
178         std::string regexp_str = ucr::toUTF8(regExp);
179
180         m_pMaskFilter->RemoveAllFilters();
181         m_pMaskFilter->AddRegExp(regexp_str);
182 }
183
184 /**
185  * @brief Check if any of filefilter rules match to filename.
186  *
187  * @param [in] szFileName Filename to test.
188  * @return true unless we're suppressing this file by filter
189  */
190 bool FileFilterHelper::includeFile(const String& szFileName) const
191 {
192         if (m_bUseMask)
193         {
194                 if (m_pMaskFilter == nullptr)
195                 {
196                         throw "Use mask set, but no filter rules for mask!";
197                 }
198
199                 // preprend a backslash if there is none
200                 String strFileName = strutils::makelower(szFileName);
201                 if (strFileName.empty() || strFileName[0] != '\\')
202                         strFileName = _T("\\") + strFileName;
203                 // append a point if there is no extension
204                 if (strFileName.find('.') == String::npos)
205                         strFileName = strFileName + _T(".");
206
207                 return m_pMaskFilter->Match(ucr::toUTF8(strFileName));
208         }
209         else
210         {
211                 if (m_fileFilterMgr == nullptr || m_currentFilter ==nullptr)
212                         return true;
213                 return m_fileFilterMgr->TestFileNameAgainstFilter(m_currentFilter, szFileName);
214         }
215 }
216
217 /**
218  * @brief Check if any of filefilter rules match to directoryname.
219  *
220  * @param [in] szFileName Directoryname to test.
221  * @return true unless we're suppressing this directory by filter
222  */
223 bool FileFilterHelper::includeDir(const String& szDirName) const
224 {
225         if (m_bUseMask)
226         {
227                 // directories have no extension
228                 return true; 
229         }
230         else
231         {
232                 if (m_fileFilterMgr == nullptr || m_currentFilter == nullptr)
233                         return true;
234
235                 // Add a backslash
236                 String strDirName(_T("\\"));
237                 strDirName += szDirName;
238
239                 return m_fileFilterMgr->TestDirNameAgainstFilter(m_currentFilter, strDirName);
240         }
241 }
242
243 /**
244  * @brief Load in all filters in a folder.
245  * @param [in] dir Folder from where to load filters.
246  * @param [in] sPattern Wildcard defining files to add to map as filter files.
247  *   It is filemask, for example, "*.flt"
248  */
249 void FileFilterHelper::LoadFileFilterDirPattern(const String& dir, const String& szPattern)
250 {
251         m_fileFilterMgr->LoadFromDirectory(dir, szPattern, FileFilterExt);
252 }
253
254 /** 
255  * @brief Convert user-given extension list to valid regular expression.
256  * @param [in] Extension list/mask to convert to regular expression.
257  * @return Regular expression that matches extension list.
258  */
259 String FileFilterHelper::ParseExtensions(const String &extensions) const
260 {
261         String strParsed;
262         String strPattern;
263         String ext(extensions);
264         bool bFilterAdded = false;
265         static const TCHAR pszSeps[] = _T(" ;|,:");
266
267         ext += _T(";"); // Add one separator char to end
268         size_t pos = ext.find_first_of(pszSeps);
269         
270         while (pos != String::npos)
271         {
272                 String token = ext.substr(0, pos); // Get first extension
273                 ext = ext.substr(pos + 1); // Remove extension + separator
274                 
275                 // Only "*." or "*.something" allowed, other ignored
276                 if (token.length() >= 1)
277                 {
278                         bFilterAdded = true;
279                         String strRegex = token;
280                         strutils::replace(strRegex, _T("."), _T("\\."));
281                         strutils::replace(strRegex, _T("?"), _T("."));
282                         strutils::replace(strRegex, _T("("), _T("\\("));
283                         strutils::replace(strRegex, _T(")"), _T("\\)"));
284                         strutils::replace(strRegex, _T("["), _T("\\["));
285                         strutils::replace(strRegex, _T("]"), _T("\\]"));
286                         strutils::replace(strRegex, _T("$"), _T("\\$"));
287                         strutils::replace(strRegex, _T("*"), _T(".*"));
288                         strRegex += _T("$");
289                         strPattern += _T("(^|\\\\)") + strRegex;
290                 }
291                 else
292                         bFilterAdded = false;
293
294                 pos = ext.find_first_of(pszSeps); 
295                 if (bFilterAdded && pos != String::npos && extensions.length() > 1)
296                         strPattern += _T("|");
297         }
298
299         if (strPattern.empty())
300                 strParsed = _T(".*"); // Match everything
301         else
302         {
303
304                 strPattern = strutils::makelower(strPattern);
305                 strParsed = strPattern; //+ _T("$");
306         }
307         return strParsed;
308 }
309
310 /** 
311  * @brief Returns active filter (or mask string)
312  * @return The active filter.
313  */
314 String FileFilterHelper::GetFilterNameOrMask() const
315 {
316         String sFilter;
317
318         if (!IsUsingMask())
319                 sFilter = GetFileFilterName(m_sFileFilterPath);
320         else
321                 sFilter = m_sMask;
322
323         return sFilter;
324 }
325
326 /** 
327  * @brief Set filter.
328  *
329  * Simple-to-use function to select filter. This function determines
330  * filter type so caller doesn't need to care about it.
331  *
332  * @param [in] filter File mask or filter name.
333  * @return true if given filter was set, false if default filter was set.
334  * @note If function returns false, you should ask filter set with
335  * GetFilterNameOrMask().
336  */
337 bool FileFilterHelper::SetFilter(const String &filter)
338 {
339         // If filter is empty string set default filter
340         if (filter.empty())
341         {
342                 UseMask(true);
343                 SetMask(_T("*.*"));
344                 SetFileFilterPath(_T(""));
345                 return false;
346         }
347
348         // Remove leading and trailing whitespace characters from the string.
349         String flt = strutils::trim_ws(filter);
350
351         // Star means we have a file extension mask
352         if (filter.find_first_of(_T("*?")) != -1)
353         {
354                 UseMask(true);
355                 SetMask(flt);
356                 SetFileFilterPath(_T(""));
357         }
358         else
359         {
360                 String path = GetFileFilterPath(flt);
361                 if (!path.empty())
362                 {
363                         UseMask(false);
364                         SetFileFilterPath(path);
365                 }
366                 // If filter not found with given name, use default filter
367                 else
368                 {
369                         UseMask(true);
370                         SetMask(_T("*.*"));
371                         SetFileFilterPath(_T(""));
372                         return false;
373                 }
374         }
375         return true;
376 }
377
378 /** 
379  * @brief Reloads changed filter files
380  *
381  * Checks if filter file has been modified since it was last time
382  * loaded/reloaded. If file has been modified we reload it.
383  * @todo How to handle an error in reloading filter?
384  */
385 void FileFilterHelper::ReloadUpdatedFilters()
386 {
387         DirItem fileInfo;
388         String selected;
389         vector<FileFilterInfo> filters = GetFileFilters(selected);
390         vector<FileFilterInfo>::const_iterator iter = filters.begin();
391         while (iter != filters.end())
392         {
393                 String path = (*iter).fullpath;
394
395                 fileInfo.Update(path);
396                 if (fileInfo.mtime != (*iter).fileinfo.mtime ||
397                         fileInfo.size != (*iter).fileinfo.size)
398                 {
399                         // Reload filter after changing it
400                         int retval = m_fileFilterMgr->ReloadFilterFromDisk(path);
401                         
402                         if (retval == FILTER_OK)
403                         {
404                                 // If it was active filter we have to re-set it
405                                 if (path == selected)
406                                         SetFileFilterPath(path);
407                         }
408                 }
409                 ++iter;
410         }
411 }
412
413 /**
414  * @brief Load any known file filters
415  * @todo Preserve filter selection? How?
416  */
417 void FileFilterHelper::LoadAllFileFilters()
418 {
419         // First delete existing filters
420         m_fileFilterMgr->DeleteAllFilters();
421
422         // Program application directory
423         m_sGlobalFilterPath = paths::ConcatPath(env::GetProgPath(), _T("Filters"));
424         paths::normalize(m_sGlobalFilterPath);
425         String pattern(_T("*"));
426         pattern += FileFilterExt;
427         LoadFileFilterDirPattern(m_sGlobalFilterPath, pattern);
428         if (strutils::compare_nocase(m_sGlobalFilterPath, m_sUserSelFilterPath) != 0)
429                 LoadFileFilterDirPattern(m_sUserSelFilterPath, pattern);
430 }
431
432 /**
433  * @brief Return path to global filters (& create if needed), or empty if cannot create
434  */
435 String FileFilterHelper::GetGlobalFilterPathWithCreate() const
436 {
437         return paths::EnsurePathExist(m_sGlobalFilterPath);
438 }
439
440 /**
441  * @brief Return path to user filters (& create if needed), or empty if cannot create
442  */
443 String FileFilterHelper::GetUserFilterPathWithCreate() const
444 {
445         return paths::EnsurePathExist(m_sUserSelFilterPath);
446 }
447