carb/filesystem/FindFiles.h

File members: carb/filesystem/FindFiles.h

// Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

#pragma once

#include "../extras/Path.h"
#include "../extras/StringProcessor.h"
#include "../extras/StringUtils.h"
#include "IFileSystem.h"
#include "../Format.h"

#include "../../omni/str/Wildcard.h"

#include <cstdint>
#include <cstring>

namespace carb
{
namespace filesystem
{

using FindFilesFlag = uint32_t;

constexpr FindFilesFlag kFindFilesFlagNone = 0x0;

constexpr FindFilesFlag kFindFilesFlagRecursive = (1 << 0);

constexpr FindFilesFlag kFindFilesFlagMatchStem = (1 << 1);

constexpr FindFilesFlag kFindFilesFlagReplaceEnvironmentVariables = (1 << 2);

using FindFilesOnFilterNonCanonicalFn = WalkAction(const char* path, void* userData);

using FindFilesOnMatchedFn = void(const char* canonical, void* userData);

using FindFilesOnExcludedFn = void(const char* canonical, void* userData);

using FindFilesOnSkippedFn = void(const char* canonical, void* userData);

using FindFilesOnSearchPathFn = void(const char* path, void* userData);

struct FindFilesArgs
{
    const char* const* searchPaths;
    uint32_t searchPathsCount;

    const char* const* matchWildcards;
    uint32_t matchWildcardsCount;

    const char* const* excludeWildcards;
    uint32_t excludeWildcardsCount;

    const char* const* ignorePrefixes;
    uint32_t ignorePrefixesCount;

    IFileSystem* fs;

    FindFilesOnFilterNonCanonicalFn* onFilterNonCanonical;
    void* onFilterNonCanonicalContext;

    FindFilesOnMatchedFn* onMatched;
    void* onMatchedContext;

    FindFilesOnExcludedFn* onExcluded;
    void* onExcludedContext;

    FindFilesOnSkippedFn* onSkipped;
    void* onSkippedContext;

    FindFilesOnSearchPathFn* onSearchPath;
    void* onSearchPathContext;

    FindFilesFlag flags;
};

inline bool findFiles(const FindFilesArgs& args);

#ifndef DOXYGEN_BUILD
namespace detail
{

struct FindFilesContext
{
    const FindFilesArgs* args;
    IFileSystem* fs;
};

inline WalkAction onFile(const DirectoryItemInfo* const info, void* userData)
{
    if (info->type == DirectoryItemType::eFile)
    {
        auto context = reinterpret_cast<const FindFilesContext*>(userData);

        // the canonicalization of a file is expensive.  here we give the user a chance to
        // tell us if we should canonicalize the file
        if (context->args->onFilterNonCanonical)
        {
            WalkAction action =
                context->args->onFilterNonCanonical(info->path, context->args->onFilterNonCanonicalContext);
            switch (action)
            {
                case WalkAction::eStop:
                    return WalkAction::eStop;

                case WalkAction::eSkip:
                    return WalkAction::eContinue;

                default: // eContinue
                    break; // fall-through
            }
        }

        std::string canonical = context->fs->makeCanonicalPath(info->path);

        extras::Path path(canonical);
        std::string target;
        if (context->args->flags & kFindFilesFlagMatchStem)
        {
            target = path.getStem();
        }
        else
        {
            target = path.getFilename();
        }

        const char* toMatch = target.c_str();

        // note, even if a pattern matches, we still have to loop through all of "ignorePrefixesCount" looking for an
        // exclusion since exclusions take precedent.
        bool matched = false;
        for (int32_t i = -1; i < int32_t(context->args->ignorePrefixesCount); ++i)
        {
            const char* prefix = "";
            if (-1 != i)
            {
                prefix = context->args->ignorePrefixes[i];
            }

            if (extras::startsWith(toMatch, prefix))
            {
                const char* strippedMatch = toMatch + std::strlen(prefix);
                if (omni::str::matchWildcards(
                        strippedMatch, context->args->matchWildcards, context->args->matchWildcardsCount))
                {
                    if (omni::str::matchWildcards(
                            strippedMatch, context->args->excludeWildcards, context->args->excludeWildcardsCount))
                    {
                        if (context->args->onExcluded)
                        {
                            context->args->onExcluded(canonical.c_str(), context->args->onExcludedContext);
                        }
                        return WalkAction::eContinue; // exclusion takes precedent. ignore the file and keep searching
                    }
                    else
                    {
                        matched = true;
                    }
                }
            }
        }

        if (matched)
        {
            if (context->args->onMatched)
            {
                context->args->onMatched(canonical.c_str(), context->args->onMatchedContext);
            }
        }
        else
        {
            if (context->args->onSkipped)
            {
                context->args->onSkipped(canonical.c_str(), context->args->onSkippedContext);
            }
        }
    }

    return WalkAction::eContinue;
}

} // namespace detail
#endif

inline bool findFiles(const FindFilesArgs& args)
{
    if (!args.searchPaths || !args.searchPathsCount)
    {
        CARB_LOG_ERROR("searchPath must be specified");
        return false;
    }

    if (!args.matchWildcards || !args.matchWildcardsCount)
    {
        CARB_LOG_ERROR("match wildcard must be specified");
        return false;
    }

    auto framework = getFramework();
    if (!framework)
    {
        CARB_LOG_ERROR("carb::Framework not active");
        return false;
    }

    IFileSystem* fs;
    if (args.fs)
    {
        fs = framework->verifyInterface<IFileSystem>(args.fs);
        if (!fs)
        {
            CARB_LOG_ERROR("incompatible carb::filesystem::IFileSystem");
            return false;
        }
    }
    else
    {
        fs = framework->tryAcquireInterface<IFileSystem>();
        if (!fs)
        {
            CARB_LOG_ERROR("unable to acquire carb::filesystem::IFileSystem");
            return false;
        }
    }

    detail::FindFilesContext userData = { &args, fs };
    for (uint32_t i = 0; i < args.searchPathsCount; ++i)
    {
        const char* dir = args.searchPaths[i];
        std::string dirWithEnv;
        if (args.flags & kFindFilesFlagReplaceEnvironmentVariables)
        {
            dirWithEnv = extras::replaceEnvironmentVariables(dir);
            dir = dirWithEnv.c_str();
        }

        extras::Path path{ dir };
        const char* fullPath = path.getStringBuffer();

        std::string tmp;
        if (!path.isAbsolute())
        {
            tmp = carb::fmt::format("{}/{}", fs->getAppDirectoryPath(), dir);
            fullPath = tmp.c_str();
        }

        if (args.onSearchPath)
        {
            args.onSearchPath(fullPath, args.onSearchPathContext);
        }

        if (args.flags & kFindFilesFlagRecursive)
        {
            fs->forEachDirectoryItemRecursive(fullPath, detail::onFile, const_cast<detail::FindFilesContext*>(&userData));
        }
        else
        {
            fs->forEachDirectoryItem(fullPath, detail::onFile, &userData);
        }
    }

    return true;
}

} // namespace filesystem
} // namespace carb