Skip to content

Add filter file support when creating Resource Groups.#18

Draft
CCPCookies wants to merge 3 commits intocarbonengine:mainfrom
CCPCookies:filtering
Draft

Add filter file support when creating Resource Groups.#18
CCPCookies wants to merge 3 commits intocarbonengine:mainfrom
CCPCookies:filtering

Conversation

@CCPCookies
Copy link
Collaborator

Changes

  • Filter file rules loading through legacy INI file format support added.
  • Filter logic matched from resfileserver and eve-resparser.
  • Documentation of filter file format added.
  • Documentation covering filter logic added.
  • Test coverage added for all known filter include scenarios.
  • Added new global filter rule which is useful for excluding .red files.
  • Filter logic for 'resfile' field not covered as it is covered by respaths logic.
  • New publicly exposed library function added to create Resource Groups with filtering.
  • Logic added to library function to allow skipping empty search directories.
  • Logic added to library to allow ascertaining compression size from remote filesystem.
  • Test coverage for library function added.
  • CLI extended to add Resource Group creation with filters.
  • Test coverage for CLI operation added.

Version

Rather than changing 'CreateFromDirectory' in library and 'create-group' in CLI. A new function and operation was added to create Resource Groups using filters. This was to keep the API stable as there are now other parties working with the origional commands. Version minor was bumped.

Extra

Filter ini file parsing logic is from PR16

Changes
-------
* Filter file rules loading through legacy INI file format support added.
* Filter logic matched from resfileserver and eve-resparser.
* Documentation of filter file format added.
* Documentation covering filter logic added.
* Test coverage added for all known filter include scenarios.
* Added new global filter rule which is useful for excluding .red files.
* Filter logic for 'resfile' field not covered as it is covered by respaths logic.
* New publicly exposed library function added to create Resource Groups with filtering.
* Logic added to library function to allow skipping empty search directories.
* Logic added to library to allow ascertaining compression size from remote filesystem.
* Test coverage for library function added.
* CLI extended to add Resource Group creation with filters.
* Test coverage for CLI operation added.

Version
-------
Rather than changing 'CreateFromDirectory' in library and 'create-group' in CLI.
A new function and operation was added to create Resource Groups using filters.
This was to keep the API stable as there are now other parties working with the origional commands.
Version minor was bumped.

Extra
-----
Filter ini file parsing logic is from PR16
if( findResult != std::string::npos )
{
// Delimiter found
value = line.substr( findResult + 2 );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this +2?
Shouldn't this be just +1?
Unless the attributeName variable contains a ":" at the end, which then has the DELIMITER added to it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removes a space, I could add this to delimiter to be clearer


#include "CreateResourceGroupFromFilterCliOperation.h"

#include <string>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


struct FilterFile
{
std::unordered_map<std::string, std::shared_ptr<Prefix>> prefixes;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE when you iterate over this std::unordered_map, you will not get the items returned in the insertion order.
I "think" you may want it to return items in insertion order as part of the ResourceFilter::SetFromFilterFileData() function, that is calling m_prefixPaths.push_back(), which is NOT guaranteed as currently implemented.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you are right, nice catch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will also add a test to enforce the order importance

/// @note No file filtering supported
Result CreateFromDirectory( const CreateResourceGroupFromDirectoryParams& params );

/// @brief Creates a ResourceGroup from a supplied filter files.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo.
... from supplied filter files. (remove the a)
or
... from a supplied filter file. (change to singular, remove s)


ParseIncludeExcludeRules( globalFiltersStr, fileData.includeRules, fileData.excludeRules );

// Get section infomration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo:
// Get section information

ParseIncludeExcludeRules( globalFiltersStr, fileData.includeRules, fileData.excludeRules );

// Get section infomration
for( const auto& sectionName : reader.Sections() )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE:
The INIReader will return Sections in alphabetical order, not the order they appear in the .ini file.
If you want to keep the sections in the order as defined in the file, you will have to read the file manually and find all the sections and then iterate over that list (instead of reader.Sections())

I.e. change it to do:
std::vectorstd::string sectionsInOrder = ManuallyReadIniFileSectionsInOrderExcludingDefault();
for( const auto& sectionName : sectionsInOrder)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I remember you saying this. I don't see a scenario where order of sections matter.

I would also not want to be doing any ini parsing manually, this needs to be supplied by a library. The lib you found so far appears to be sort of ok. Another annoying thing it does is removes all the casing from the section names, not huge but I'd prefer it didn't.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It's annoying that it lowercases everything.
It also cuts each sectionName to the first either 48 or 50 characters (can't remember which)
But this ini reader was the best fit, because it:

  • had vcpkg support
  • emulated the original python ini file implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for if order of sections matter.
What about if the same respath and prefix are present in two different [namedSections], but one of them has an [someFilter] (include) where the other has the same ![someFilter] (exclude filter)?
What happens there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will match as it would with previous system


1. Globally in the ``[DEFAULT]`` section using ``filter =`` .
2. Section locally in sections using ``filter =`` .
3. Semi locally to respath adding include/exclude rules to each path ``respaths = prefix1:/* [ include ] ![ exclude ]``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct,
but it renders strange when viewed in a browser (splits the line).
It would be better if the "respaths = prefix1:/* [ include ] ![ exclude ]" would be put on the line below.


2. Section local filters are combined with any filters specified in global filters.

3. respaths filters combine with both global and section filters and importantly these add for all subsequent paths. This is explained more in the following examples.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this supposed to be:
"...and importantly these add for all subsequent paths WITHIN THE SELECTED SECTION."
I.e. filter defined in [SectionA] will not also be applied to all entries in [SectionB] onwards.

Two paths will be tested for inclusion:

``#3`` will use ``respaths = prefix1:/*`` and combine global and section local patterns ``include1`` and ``include2``. This will match the following from the source files:
1. ``include1.txt``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an empty line before the "1. include1.txt", for it to render correctly in a browser.

2. ``include2.txt``

``#4`` will use ``respaths = prefix1:/* [ include3 ]`` which will extend the section local patterns to include ``include3``. This will match the following source files:
1. ``include1.txt``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add empty line, so it renders correctly in a browser.

3. ``include3.txt``

``#5`` will use ``respaths = prefix2:/*`` and doesn't sepecify any include rules. It will apply the include rules that have been constructed for the section at this point ``include1``, ``include2`` and ``include3``. This may be suprising. So this will match the following source files:
1. ``Path/include3.txt``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add empty line to it renders correctly in browser.


[exampleSection]
filter = [ include2 ] # 2. Section local include
respaths = prefix1:/* # 3. respath1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE this is not how multiple (multi-line) respaths are defined in existing res.ini files.
The correct example would look like:

[resCharacterMisc]
respaths = res:/Graphics/Character/Global/...
                  res:/Graphics/Character/Female/Skeleton/...
                  res:/Graphics/Character/Female/*
                  res:/Graphics/Character/Male/Skeleton/...
                  res:/Graphics/Character/Male/*
                  res:/Graphics/Character/Unique/...

Where the multi-line entries are within the SAME "respaths" attribute.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, I'll change the documentation. The actual tests don't do this, this is just documentation.

#include <unordered_set>

CreateResourceGroupFromFilterCliOperation::CreateResourceGroupFromFilterCliOperation() :
CliOperation( "create-group-from-filter", "Create a Resource Group from a filter files." ),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixing singular and plural

#include <Md5ChecksumStream.h>
#include <GzipCompressionStream.h>
#include <cctype>
#include "ResourceInfo/PatchResourceGroupInfo.h"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return Result{ ResultType::FAILED_TO_INITIALIZE_RESOURCE_FILTER, errorMsg };
}

statusSettings.Update( StatusProgressType::PERCENTAGE, 0, 5, "Loading filter files" );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This status line has 0, 5 like the line for "Create resource group from filters".
Should the numbers be updated (is this a copy-paste error)?


if( inputDirectoryStatus.RequiresStatusUpdates() )
{
float step = static_cast<float>( 100.0 / searchPaths.size() );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step variable is going to be the same in every iteration of the loop.
Can be calculated outside fo the for loop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, this way that computation is skipped if not verbose, so we don't calculate something we don't need when not caring about the output.

}
else
{
return Result{ ResultType::INPUT_DIRECTORY_DOESNT_EXIST, inputDirectory.string() };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused.
Why would you ever not want to skip non existent input directories?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it only for testing/debug purposes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this is due to real world usecase of reduced-resources.

It only syncs files that changed, so in theory it might (and usually does) not a single file in a search directory. If it's not synced then there is no directory. But this is not a fail case.


ss << "Processing file: "
<< filePathRelativeToInputDirectory.string()
<< ", Match filter: "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Match filter:" vs matchSection
I guess this is supposed to be "Match Section:" or "Match Section Id:"
Unless you also reference return the "current include/exclude filter from the CheckPath() function and return that as well. Might be useful for debugging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually was thinking to return the current line number for the path rule so that you can see really well. But not required any further information so didn't bother to skip some computation time.

resourceParams.binaryOperation = ResourceTools::CalculateBinaryOperation( entry.path() );
}

Location l;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the name of the variable to be more descriptive?


ResourceTools::Response downloadResponse;

downloadResponse = downloader.GetHeader( resourceUrl, params.compressionCalculationSettings.downloadSettings.retryCount, params.compressionCalculationSettings.downloadSettings.retrySeconds, response );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question?
Is this the slow part?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is actually a speedy part.

This stops you needing to calculate the compression data again.

}
catch( std::invalid_argument& )
{
resourceProcessGranular.Update( CarbonResources::StatusProgressType::WARNING, 0, 0, "Invalid compression data from header information, compression will be calculated." + resourceUrl );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused.
Where is the compression information being calculated?
Don't you have to
calculateCompressions = true;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it can get the compression data from the download header then it takes it from there.

unsigned long in = std::stoul( contentLengthStr );
if( in > std::numeric_limits<uint32_t>::max() )
{
resourceProcessGranular.Update( CarbonResources::StatusProgressType::WARNING, 0, 0, "Invalid compression data from header information, compression will be calculated." + resourceUrl );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question, see comment in the catch block below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question? Below just also says 'same' :D

}
catch( std::out_of_range& )
{
resourceProcessGranular.Update( CarbonResources::StatusProgressType::WARNING, 0, 0, "Invalid compression data from header information, compression will be calculated." + resourceUrl );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??


#include <filesystem>
#include <vector>
#include <unordered_map>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports in alphabetical order


struct FilterPath
{
std::string sectionId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other structs / classes you've put an empty line between items.
Missing in this struct.

std::string prefixId;
std::string path;
std::string matchPattern;
std::set<std::string> includeRules;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:
Sets are good as they enforce uniqueness.
But is there ever a chance that the order of individual include/exclude elements matters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter

bool containsLocalIncludeExcludeRules;
};

class ResourceFilter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


~ResourceFilter();

bool SetFromFilterFileData( const FilterFile& fileData );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
}

void FilterFileReader::LoadFromIniFileData( const char* data, size_t dataSize, FilterFile& fileData )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment to include class member function comments for input/output parameters and functionality.
https://didactic-adventure-egnoryz.pages.github.io/cpp_coding_guidelines.html#commenting-class-member-functions

}
}

void FilterFileReader::ParsePrefixMappings( const std::string& prefixStr, std::unordered_map<std::string, std::shared_ptr<Prefix>>& prefixes )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already mentioned.
Order of entries in prefixmap are not the same as insert order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, will be changing


ParseIncludeExcludeRules( filter, filterSection->includeRules, filterSection->excludeRules );

// Respaths is required
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're missing reading the optional "resfile" attribute from the .ini file.
The "resfile" attribute behaves just like the "respaths" attribute.
The only difference being that each [NamedSection] can only have a single "resfile" and "respaths" attributes.
But "respaths" can be multi-line, where as the optional "resfile" attribute is a single-line entry only.

NOTE, "resfile" is intended to define a single file, which can also be one of the multi-line entries of a "respath".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I retired it :D

void FilterFileReader::ParseIncludeExcludeRules( const std::string& rulesStr, std::set<std::string>& includeRules, std::set<std::string>& excludeRules )
{

std::string s = rulesStr;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "ruleStr" variable could just be passed into this function by value and then you could just use it directly, instead of doing the extra:
std::string s = ruleStr;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly not really looked at this code too closely, it's pretty much just hooking up the code from your PR. I'll do a pass on it before a take this PR out of draft.

if( pathPart.find( "../" ) != std::string::npos )
{
// Escaping is not supported in respaths
throw std::invalid_argument( "Escaping paths not supported for respaths: " + rawPathLine );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lightbulb moment.
Yeah, you're right.
That makes it so much simpler.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is never done in our files and also isn't a good pattern as it should be done in the prefixes anyway


#include "ResourceFilter.h"

#include <regex>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alphabetical order of includes

namespace ResourceTools
{

ResourceFilter::ResourceFilter()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor and destructor could be replaced with this definition in the header file:

ResourceFilter() = default;
~ResourceFilter() = default;

namespace ResourceTools
{

FilterFileReader::FilterFileReader()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be replaced with this in the header file:

FilterFileReader() = default;
~FilterFileReader() = default;

m_paths.clear();

// Populate prefix paths
for( auto& prefix : fileData.prefixes )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already mentioned:
fileData.prefixes is not ordered in the insertion order.

}

// Populate search paths from filter data
for( auto& filterSection : fileData.filterSections )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note (but I think you've said it doesn't matter), but adding it as a comment to the review, just in case:
fileData.filterSections gets populated in the order returned from
"reader.Sections()", which means it is returned in alphabetical order, not actual order as defined in the .ini file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter for our application

std::unique_ptr<FilterPath> filterPath = std::make_unique<FilterPath>();

// Normalise path and convert to pattern
std::string prefixPathStr = prefixPath.string();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using prefixPath.lexically_normal.generic_string()?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It "should" take care of all the . \ and / checks you're manually doing in the lines below.

return true;
}

void ResourceFilter::ConvertResPathToPattern( const std::string& resPath, std::string& pattern ) const
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


void ResourceFilter::ConvertResPathToPattern( const std::string& resPath, std::string& pattern ) const
{
std::string resPathString = resPath;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If resPath is passed by value, this is not needed.

return true;
}

void ResourceFilter::ConvertResPathToPattern( const std::string& resPath, std::string& pattern ) const
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just retrun "pattern", instead of it being a reference variable?

return CheckPath( path, sectionId, matchPath );
}

bool ResourceFilter::CheckPath( const std::filesystem::path& path, std::string& matchSectionId, std::string& matchPath ) const
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
for( auto& filterPath : m_paths )
{
std::string resolvedPathStr = path.string();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible simplification.
Have you considered using prefixPath.lexically_normal.generic_string()?
It should sort out the extra checks being done below.

}
}

// Excludes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't exclude rules be checked before include rules?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants