-/////////////////////////////////////////////////////////////////////////////
-// FileFilterMgr.cpp : implementation file
-// see FileFilterMgr.h for description
-/////////////////////////////////////////////////////////////////////////////
-// License (GPLv2+):
-// This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
-// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-// You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-/////////////////////////////////////////////////////////////////////////////
-//
-
-#include "stdafx.h"
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file FileFilterMgr.cpp
+ *
+ * @brief Implementation of FileFilterMgr and supporting routines
+ */
+
+#include "pch.h"
#include "FileFilterMgr.h"
-#include "RegExp.h"
+#include <vector>
+#include <Poco/String.h>
+#include <Poco/Glob.h>
+#include <Poco/RegularExpression.h>
+#include "DirTravel.h"
+#include "DirItem.h"
+#include "UnicodeString.h"
+#include "FileFilter.h"
+#include "UniFile.h"
+#include "paths.h"
-#ifdef _DEBUG
-#define new DEBUG_NEW
-#undef THIS_FILE
-static char THIS_FILE[] = __FILE__;
-#endif
+using std::vector;
+using Poco::Glob;
+using Poco::icompare;
+using Poco::RegularExpression;
-// I think that CRegExp doesn't copy correctly (I get heap corruption in CRegList::program)
-// so I'm using pointers to avoid its copy constructor
-// Perry, 2003-05-18
+static void AddFilterPattern(vector<FileFilterElementPtr> *filterList, String & str, bool fileFilter);
-typedef CTypedPtrList<CPtrList, CRegExp*>RegList;
-static void DeleteRegList(RegList & reglist)
+/**
+ * @brief Destructor, frees all filters.
+ */
+FileFilterMgr::~FileFilterMgr()
{
- while (!reglist.IsEmpty())
- {
- CRegExp * regexp = reglist.RemoveHead();
- delete regexp;
- }
+ DeleteAllFilters();
}
-// One actual filter
-// For example, this might be a GNU C filter, excluding *.o files and CVS directories
-// That is to say, a filter is a set of file masks and directory masks
-struct FileFilter
-{
- bool default_include;
- CString name;
- CString fullpath;
- RegList filefilters;
- RegList dirfilters;
- FileFilter() : default_include(true) { }
- ~FileFilter();
-};
-FileFilter::~FileFilter()
+
+/**
+ * @brief Loads filterfile from disk and adds it to filters.
+ * @param [in] szFilterFile Filter file to load.
+ * @return FILTER_OK if succeeded or one of FILTER_RETVALUE values on error.
+ */
+int FileFilterMgr::AddFilter(const String& szFilterFile)
{
- DeleteRegList(filefilters);
- DeleteRegList(dirfilters);
+ int errorcode = FILTER_OK;
+ FileFilter * pFilter = LoadFilterFile(szFilterFile, errorcode);
+ if (pFilter != nullptr)
+ m_filters.push_back(FileFilterPtr(pFilter));
+ return errorcode;
}
-FileFilterMgr::~FileFilterMgr()
+/**
+ * @brief Load all filter files matching pattern from disk into internal filter set.
+ * @param [in] dir Directory from where filters are loaded.
+ * @param [in] szPattern Pattern for filters to load filters, for example "*.flt".
+ * @param [in] szExt File-extension of filter files.
+ */
+void FileFilterMgr::LoadFromDirectory(const String& dir, const String& szPattern, const String& szExt)
{
- DeleteAllFilters();
+ try
+ {
+ DirItemArray dirs, files;
+ LoadAndSortFiles(dir, &dirs, &files, false);
+ Glob glb(ucr::toUTF8(szPattern));
+
+ for (DirItem& item: files)
+ {
+ String filename = item.filename;
+ if (!glb.match(ucr::toUTF8(filename)))
+ continue;
+ if (!szExt.empty())
+ {
+ // caller specified a specific extension
+ // (This is really a workaround for brokenness in windows, which
+ // doesn't screen correctly on extension in pattern)
+ const String ext = filename.substr(filename.length() - szExt.length());
+ if (strutils::compare_nocase(szExt, ext) != 0)
+ return;
+ }
+
+ String filterpath = paths::ConcatPath(dir, filename);
+ AddFilter(filterpath);
+ }
+ }
+ catch (...)
+ {
+ }
}
-// Load
-void FileFilterMgr::LoadFromDirectory(LPCTSTR szPattern, LPCTSTR szExt)
+/**
+ * @brief Removes filter from filterlist.
+ *
+ * @param [in] szFilterFile Filename of filter to remove.
+ */
+void FileFilterMgr::RemoveFilter(const String& szFilterFile)
{
- DeleteAllFilters();
- CFileFind finder;
- BOOL bWorking = finder.FindFile(szPattern);
- int extlen = szExt ? _tcslen(szExt) : 0;
- while (bWorking)
+ // Note that m_filters.GetSize can change during loop
+ vector<FileFilterPtr>::iterator iter = m_filters.begin();
+ while (iter != m_filters.end())
{
- bWorking = finder.FindNextFile();
- if (finder.IsDots() || finder.IsDirectory())
- continue;
- CString sFilename = finder.GetFileName();
- if (szExt)
+ if (strutils::compare_nocase((*iter)->fullpath, szFilterFile) == 0)
{
- // caller specified a specific extension
- // (This is really a workaround for brokenness in windows, which
- // doesn't screen correctly on extension in pattern)
- if (sFilename.Right(extlen).CompareNoCase(szExt))
- return;
+ m_filters.erase(iter);
+ break;
}
- FileFilter * pfilter = LoadFilterFile(finder.GetFilePath(), sFilename);
- m_filters.Add(pfilter);
+ ++iter;
}
}
+/**
+ * @brief Removes all filters from current list.
+ */
void FileFilterMgr::DeleteAllFilters()
{
- for (int i=0; i<m_filters.GetSize(); ++i)
- {
- delete m_filters[i];
- m_filters[i] = 0;
- }
- m_filters.RemoveAll();
+ m_filters.clear();
}
-// Add a single pattern (if nonempty & valid) to a pattern list
-static void AddFilterPattern(RegList & reglist, CString & str)
+/**
+ * @brief Add a single pattern (if nonempty & valid) to a pattern list.
+ *
+ * @param [in] filterList List where pattern is added.
+ * @param [in] str Temporary variable (ie, it may be altered)
+ */
+static void AddFilterPattern(vector<FileFilterElementPtr> *filterList, String & str, bool fileFilter)
{
- str.TrimLeft();
- str.MakeUpper();
- LPCTSTR commentLeader = _T(" ##");
- // anything from commentLeader to end of line is a comment
- int comment = str.Find(commentLeader);
- if (comment >= 0)
+ const String& commentLeader = _T("##"); // Starts comment
+ str = strutils::trim_ws_begin(str);
+
+ // Ignore lines beginning with '##'
+ size_t pos = str.find(commentLeader);
+ if (pos == 0)
+ return;
+
+ // Find possible comment-separator '<whitespace>##'
+ while (pos != std::string::npos && !(str[pos - 1] == ' ' || str[pos - 1] == '\t'))
+ pos = str.find(commentLeader, pos + 1);
+
+ // Remove comment and whitespaces before it
+ if (pos != std::string::npos)
+ str = str.substr(0, pos);
+ str = strutils::trim_ws_end(str);
+ if (str.empty())
+ return;
+
+ int re_opts = RegularExpression::RE_CASELESS;
+ std::string regexString = ucr::toUTF8(str);
+ re_opts |= RegularExpression::RE_UTF8;
+ try
{
- str = str.Left(comment);
+ filterList->push_back(FileFilterElementPtr(new FileFilterElement(regexString, re_opts, fileFilter)));
+ }
+ catch (...)
+ {
+ // TODO:
}
- if (str.IsEmpty()) return;
- CRegExp * regexp = new CRegExp;
- if (regexp->RegComp(str))
- reglist.AddTail(regexp);
- else
- delete regexp;
}
-// Parse a filter file, and add it to array if valid
-FileFilter * FileFilterMgr::LoadFilterFile(LPCTSTR szFilepath, LPCTSTR szFilename)
+/**
+ * @brief Parse a filter file, and add it to array if valid.
+ *
+ * @param [in] szFilePath Path (w/ filename) to file to load.
+ * @param [out] error Error-code if loading failed (returned `nullptr`).
+ * @return Pointer to new filter, or `nullptr` if error (check error code too).
+ */
+FileFilter * FileFilterMgr::LoadFilterFile(const String& szFilepath, int & error)
{
- CStdioFile file;
- if (!file.Open(szFilepath, CFile::modeRead))
- return NULL;
+ UniMemFile file;
+ if (!file.OpenReadOnly(szFilepath))
+ {
+ error = FILTER_ERROR_FILEACCESS;
+ return nullptr;
+ }
+
+ file.ReadBom(); // in case it is a Unicode file, let UniMemFile handle BOM
+ if (!file.IsUnicode() && !ucr::CheckForInvalidUtf8(
+ reinterpret_cast<const char*>(file.GetBase()), static_cast<size_t>(file.GetFileSize())))
+ file.SetUnicoding(ucr::UTF8);
+
+ String fileName;
+ paths::SplitFilename(szFilepath, nullptr, &fileName, nullptr);
FileFilter *pfilter = new FileFilter;
pfilter->fullpath = szFilepath;
- pfilter->name = szFilename; // default if no name
- CString sLine;
- while (file.ReadString(sLine))
+ pfilter->name = std::move(fileName); // Filename is the default name
+
+ String sLine;
+ bool lossy = false;
+ bool bLinesLeft = true;
+ do
{
- if (0 == _tcsncmp(sLine, _T("name:"), 5))
+ // Returns false when last line is read
+ String tmpLine;
+ bLinesLeft = file.ReadString(tmpLine, &lossy);
+ sLine = std::move(tmpLine);
+ sLine = strutils::trim_ws(sLine);
+
+ if (0 == sLine.compare(0, 5, _T("name:"), 5))
{
// specifies display name
- CString str = sLine.Mid(5);
- str.TrimLeft();
- if (!str.IsEmpty())
- pfilter->name = str;
+ String str = sLine.substr(5);
+ str = strutils::trim_ws_begin(str);
+ if (!str.empty())
+ pfilter->name = std::move(str);
}
- else if (0 == _tcsncmp(sLine, _T("def:"), 4))
+ else if (0 == sLine.compare(0, 5, _T("desc:"), 5))
+ {
+ // specifies display name
+ String str = sLine.substr(5);
+ str = strutils::trim_ws_begin(str);
+ if (!str.empty())
+ pfilter->description = std::move(str);
+ }
+ else if (0 == sLine.compare(0, 4, _T("def:"), 4))
{
// specifies default
- CString str = sLine.Mid(4);
- str.TrimLeft();
+ String str = sLine.substr(4);
+ str = strutils::trim_ws_begin(str);
if (str == _T("0") || str == _T("no") || str == _T("exclude"))
pfilter->default_include = false;
else if (str == _T("1") || str == _T("yes") || str == _T("include"))
pfilter->default_include = true;
}
- else if (0 == _tcsncmp(sLine, _T("f:"), 2))
+ else if (0 == sLine.compare(0, 2, _T("f:"), 2))
{
// file filter
- CString str = sLine.Mid(2);
- AddFilterPattern(pfilter->filefilters, str);
+ String str = sLine.substr(2);
+ AddFilterPattern(&pfilter->filefilters, str, true);
}
- else if (0 == _tcsncmp(sLine, _T("d:"), 2))
+ else if (0 == sLine.compare(0, 2, _T("d:"), 2))
{
// directory filter
- CString str = sLine.Mid(2);
- AddFilterPattern(pfilter->dirfilters, str);
+ String str = sLine.substr(2);
+ AddFilterPattern(&pfilter->dirfilters, str, false);
}
- }
+ else if (0 == sLine.compare(0, 3, _T("f!:"), 3))
+ {
+ // file filter
+ String str = sLine.substr(3);
+ AddFilterPattern(&pfilter->filefiltersExclude, str, true);
+ }
+ else if (0 == sLine.compare(0, 3, _T("d!:"), 3))
+ {
+ // directory filter
+ String str = sLine.substr(3);
+ AddFilterPattern(&pfilter->dirfiltersExclude, str, false);
+ }
+ } while (bLinesLeft);
+
return pfilter;
}
-// Give client back a pointer to the actual filter
-// We just do a linear search, because this is seldom called
-FileFilter * FileFilterMgr::GetFilter(LPCTSTR szFilterName)
+/**
+ * @brief Give client back a pointer to the actual filter.
+ *
+ * @param [in] szFilterPath Full path to filterfile.
+ * @return Pointer to found filefilter or `nullptr`;
+ * @note We just do a linear search, because this is seldom called
+ */
+FileFilter * FileFilterMgr::GetFilterByPath(const String& szFilterPath)
{
- for (int i=0; i<m_filters.GetSize(); ++i)
+ vector<FileFilterPtr>::const_iterator iter = m_filters.begin();
+ while (iter != m_filters.end())
{
- if (m_filters[i]->name == szFilterName)
- return m_filters[i];
+ if (strutils::compare_nocase((*iter)->fullpath, szFilterPath) == 0)
+ return (*iter).get();
+ ++iter;
}
return 0;
}
-// return TRUE if string passes
-BOOL TestAgainstRegList(const RegList & reglist, LPCTSTR szTest)
+/**
+ * @brief Give client back a pointer to the actual filter.
+ *
+ * @param [in] i Index of filter.
+ * @return Pointer to filefilter in given index or `nullptr`.
+ */
+FileFilter * FileFilterMgr::GetFilterByIndex(int i)
+{
+ if (i < 0 || i >= m_filters.size())
+ return nullptr;
+
+ return m_filters[i].get();
+}
+
+/**
+ * @brief Test given string against given regexp list.
+ *
+ * @param [in] filterList List of regexps to test against.
+ * @param [in] szTest String to test against regexps.
+ * @return true if string passes
+ * @note Matching stops when first match is found.
+ */
+bool TestAgainstRegList(const vector<FileFilterElementPtr> *filterList, const String& szTest)
{
- CString str = szTest;
- str.MakeUpper();
- for (POSITION pos = reglist.GetHeadPosition(); pos; )
+ if (filterList->size() == 0)
+ return false;
+
+ std::string compString, compStringFileName;
+ ucr::toUTF8(szTest, compString);
+ vector<FileFilterElementPtr>::const_iterator iter = filterList->begin();
+ while (iter != filterList->end())
{
- CRegExp * regexp = reglist.GetNext(pos);
- if (regexp->RegFind(str) != -1)
- return TRUE;
+ RegularExpression::Match match;
+ try
+ {
+ if ((*iter)->_fileNameOnly && compStringFileName.empty())
+ ucr::toUTF8(paths::FindFileName(szTest), compStringFileName);
+ if ((*iter)->regexp.match((*iter)->_fileNameOnly ? compStringFileName : compString, 0, match) > 0)
+ return true;
+ }
+ catch (...)
+ {
+ // TODO:
+ }
+
+ ++iter;
}
- return FALSE;
+ return false;
}
-// return TRUE if file passes the filter
-BOOL FileFilterMgr::TestFileNameAgainstFilter(FileFilter * pFilter, LPCTSTR szFileName)
+/**
+ * @brief Test given filename against filefilter.
+ *
+ * Test filename against active filefilter. If matching rule is found
+ * we must first determine type of rule that matched. If we return false
+ * from this function directory scan marks file as skipped.
+ *
+ * @param [in] pFilter Pointer to filefilter
+ * @param [in] szFileName Filename to test
+ * @return true if file passes the filter
+ */
+bool FileFilterMgr::TestFileNameAgainstFilter(const FileFilter * pFilter,
+ const String& szFileName) const
{
- if (!pFilter) return TRUE;
- if (TestAgainstRegList(pFilter->filefilters, szFileName))
- return !pFilter->default_include;
+ if (pFilter == nullptr)
+ return true;
+ if (TestAgainstRegList(&pFilter->filefilters, szFileName))
+ {
+ if (pFilter->filefiltersExclude.empty() || !TestAgainstRegList(&pFilter->filefiltersExclude, szFileName))
+ return !pFilter->default_include;
+ }
return pFilter->default_include;
}
-// return TRUE if directory passes the filter
-BOOL FileFilterMgr::TestDirNameAgainstFilter(FileFilter * pFilter, LPCTSTR szDirName)
+/**
+ * @brief Test given directory name against filefilter.
+ *
+ * Test directory name against active filefilter. If matching rule is found
+ * we must first determine type of rule that matched. If we return false
+ * from this function directory scan marks file as skipped.
+ *
+ * @param [in] pFilter Pointer to filefilter
+ * @param [in] szDirName Directory name to test
+ * @return true if directory name passes the filter
+ */
+bool FileFilterMgr::TestDirNameAgainstFilter(const FileFilter * pFilter,
+ const String& szDirName) const
{
- if (!pFilter) return TRUE;
- if (TestAgainstRegList(pFilter->dirfilters, szDirName))
- return !pFilter->default_include;
+ if (pFilter == nullptr)
+ return true;
+ if (TestAgainstRegList(&pFilter->dirfilters, szDirName))
+ {
+ if (pFilter->dirfiltersExclude.empty() || !TestAgainstRegList(&pFilter->dirfiltersExclude, szDirName))
+ return !pFilter->default_include;
+ }
return pFilter->default_include;
}
-CString FileFilterMgr::GetFilterName(int i)
+/**
+ * @brief Reload filter from disk
+ *
+ * Reloads filter from disk. This is done by creating a new one
+ * to substitute for old one.
+ * @param [in] pFilter Pointer to filter to reload.
+ * @return FILTER_OK when succeeds, one of FILTER_RETVALUE values on error.
+ * @note Given filter (pfilter) is freed and must not be used anymore.
+ * @todo Should return new filter.
+ */
+int FileFilterMgr::ReloadFilterFromDisk(FileFilter * pfilter)
{
- return m_filters[i]->name;
+ int errorcode = FILTER_OK;
+ FileFilter * newfilter = LoadFilterFile(pfilter->fullpath, errorcode);
+
+ if (newfilter == nullptr)
+ {
+ return errorcode;
+ }
+
+ vector<FileFilterPtr>::iterator iter = m_filters.begin();
+ while (iter != m_filters.end())
+ {
+ if (pfilter == (*iter).get())
+ {
+ m_filters.erase(iter);
+ break;
+ }
+ }
+ m_filters.push_back(FileFilterPtr(newfilter));
+ return errorcode;
}
-CString FileFilterMgr::GetFullpath(FileFilter * pfilter) const
+/**
+ * @brief Reload filter from disk.
+ *
+ * Reloads filter from disk. This is done by creating a new one
+ * to substitute for old one.
+ * @param [in] szFullPath Full path to filter file to reload.
+ * @return FILTER_OK when succeeds or one of FILTER_RETVALUE values when fails.
+ */
+int FileFilterMgr::ReloadFilterFromDisk(const String& szFullPath)
{
- return pfilter->fullpath;
+ int errorcode = FILTER_OK;
+ FileFilter * filter = GetFilterByPath(szFullPath);
+ if (filter)
+ errorcode = ReloadFilterFromDisk(filter);
+ else
+ errorcode = FILTER_NOTFOUND;
+ return errorcode;
}
-// Reload filter from disk (by creating a new one to substitute for old one)
-void FileFilterMgr::ReloadFilterFromDisk(FileFilter * pfilter)
+/**
+ * @brief Clone file filter manager from another file filter Manager.
+ * This function clones file filter manager from another file filter manager.
+ * Current contents in the file filter manager are removed and new contents added from the given file filter manager.
+ * @param [in] fileFilterManager File filter manager to clone.
+ */
+void FileFilterMgr::CloneFrom(const FileFilterMgr* fileFilterMgr)
{
- FileFilter * newfilter = LoadFilterFile(pfilter->fullpath, pfilter->name);
- for (int i=0; i<m_filters.GetSize(); ++i)
+ if (!fileFilterMgr)
+ return;
+
+ m_filters.clear();
+
+ size_t count = fileFilterMgr->m_filters.size();
+ for (size_t i = 0; i < count; i++)
{
- if (pfilter == m_filters[i])
- {
- m_filters.RemoveAt(i);
- delete pfilter;
- break;
- }
+ auto ptr = std::make_shared<FileFilter>(FileFilter());
+ ptr->CloneFrom(fileFilterMgr->m_filters[i].get());
+ m_filters.push_back(ptr);
}
- m_filters.Add(newfilter);
-}
\ No newline at end of file
+}